Skip to content
Latchkey

Custard CI workflow (GoogleCloudPlatform/nodejs-docs-samples)

The Custard CI workflow from GoogleCloudPlatform/nodejs-docs-samples, explained and optimized by Latchkey.

C

CI health: C - fair

Point runs-on at Latchkey and get caching, run de-duplication, self-healing for flaky steps, and up to 58% lower cost, applied automatically.

Grade your own workflow free or run it on Latchkey →
Source: GoogleCloudPlatform/nodejs-docs-samples.github/workflows/custard-ci.yamlLicense Apache-2.0View source

What it does

This is the Custard CI workflow from the GoogleCloudPlatform/nodejs-docs-samples repository, a real project running GitHub Actions. It is shown here with attribution under its Apache-2.0 license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# IMPORTANT: DO NOT CHANGE THE NAME OF THIS WORKFLOW!
# The workflow named "Custard CI" triggers both custard-run.yaml and custard-run-dev.yaml.
# TODO: To sunset old tests remove `affected`, `lint`, and `test` jobs (only keep `region-tags`).
name: Custard CI
on:
  push:
    branches:
      - main
  pull_request:
  # schedule:
  #   # https://crontab.guru/#0_12_*_*_0
  #   - cron: 0 12 * * 0 # At 12:00 on Sunday

env:
  GO_VERSION: ^1.22.0

# TODO: remove all jobs except region-tags (should be tested by custard-run workflows)
jobs:
  affected:
    name: (legacy) Finding affected tests
    runs-on: ubuntu-latest
    timeout-minutes: 2
    outputs:
      nodejs-paths: ${{ steps.nodejs.outputs.paths }}
      nodejs-setups: ${{ steps.nodejs.outputs.setups }}
    steps:
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
        with:
          fetch-depth: 0
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
        with:
          repository: GoogleCloudPlatform/cloud-samples-tools
          ref: v0.3.2
          path: cloud-samples-tools
      - name: Create `bin` directory for cloud-samples-tools binaries
        run: mkdir bin
        working-directory: cloud-samples-tools
      - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5
        with:
          go-version: ${{ env.GO_VERSION }}
      - name: Build Custard (from cloud-samples-tools)
        run: go build -o ../bin -v ./...
        working-directory: cloud-samples-tools/custard
      - name: Get diffs
        run: git --no-pager diff --name-only HEAD origin/main | tee diffs.txt
      - name: Find Node.js affected packages
        id: nodejs
        run: |
          echo "paths=$(./cloud-samples-tools/bin/custard affected .github/config/nodejs.jsonc diffs.txt paths.txt)" >> $GITHUB_OUTPUT
          cat paths.txt
          echo "setups=$(./cloud-samples-tools/bin/custard setup-files .github/config/nodejs.jsonc paths.txt)" >> $GITHUB_OUTPUT

  lint:
    needs: affected
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - name: Checkout
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
      - name: Setup Node
        uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
        with:
          node-version: 20
      - run: npm install
      - name: Run lint
        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
        with:
          script: |
            const { execSync } = await import("node:child_process");

            const cmd = 'npx gts lint';
            const affected = ${{ needs.affected.outputs.nodejs-paths }};
            if (affected.length === 0) {
              console.log("No packages were affected, nothing to lint.")
            }

            let failed = [];
            for (const path of affected) {
              try {
                execSync(cmd, {cwd: path});
                console.log(`✅ [${path}]: ${cmd}`);
              } catch (e) {
                failed.push(path)
                console.log(`❌ [${path}]: ${cmd} (exit code ${e.status})`);
                core.error(e.message);
                console.log('--- stdout ---');
                console.log(e.stdout.toString("utf8"));
                console.log('--- stderr ---');
                console.log(e.stderr.toString("utf8"));
              }
            }
            console.log("=== Summary ===")
            console.log(`  Passed: ${affected.length - failed.length}`)
            console.log(`  Failed: ${failed.length}`)
            if (failed.length > 0) {
              core.setFailed(`Failed '${cmd}' on: ${failed.join(', ')}`)
            }

  region-tags:
    name: region tags
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
        with:
          node-version: 20
      - run: ./.github/workflows/utils/region-tags-tests.sh

  test:
    if: github.event.pull_request && github.event.pull_request.head.repo.fork == false
    name: (legacy) test
    needs: affected
    runs-on: ubuntu-latest
    timeout-minutes: 120 # 2 hours hard limit
    permissions:
      id-token: write # needed for google-github-actions/auth
    strategy:
      fail-fast: false
      matrix:
        path: ${{ fromJson(github.event_name == 'pull_request' && needs.affected.outputs.nodejs-paths || '[]') }}
    env:
      GOOGLE_SAMPLES_PROJECT: long-door-651
      GOOGLE_SERVICE_ACCOUNT: kokoro-system-test@long-door-651.iam.gserviceaccount.com
      CI_SETUP: ${{ toJson(fromJson(needs.affected.outputs.nodejs-setups)[matrix.path])}}
    steps:
      - name: CI Setup
        run: echo "${{ env.CI_SETUP }}"
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
        with:
          node-version: ${{ fromJson(env.CI_SETUP).node-version }}
      - uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3
        id: auth
        with:
          project_id: ${{ env.GOOGLE_SAMPLES_PROJECT }}
          workload_identity_provider: projects/1046198160504/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider
          service_account: ${{ env.GOOGLE_SERVICE_ACCOUNT }}
          access_token_lifetime: 600s # 10 minutes
          token_format: 'id_token'
          id_token_audience: 'https://action.test/' # service must have this custom audience
          id_token_include_email: true
      - name: Export environment variables
        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
        id: vars
        with:
          script: |
            const { default: setupVars } = await import('${{ github.workspace }}/.github/scripts/setup-vars.js');
            const projectId = '${{ env.GOOGLE_SAMPLES_PROJECT }}';
            const setup = JSON.parse(process.env.CI_SETUP);
            const serviceAccount = '${{ env.GOOGLE_SERVICE_ACCOUNT }}';
            const idToken = '${{ steps.auth.outputs.id_token }}';
            return await setupVars({projectId, core, setup, serviceAccount, idToken})
      - uses: google-github-actions/get-secretmanager-secrets@e5bb06c2ca53b244f978d33348d18317a7f263ce # v2
        if: ${{ fromJson(steps.vars.outputs.result).secrets }}
        with:
          secrets: ${{ fromJson(steps.vars.outputs.result).secrets }}
          export_to_environment: true
      - name: Run tests for ${{ matrix.path }}
        run: |
          timeout ${{ fromJson(env.CI_SETUP).timeout-minutes }}m \
            make test dir=${{ matrix.path }}

The same workflow, on Latchkey

Estimated ~20% faster on cache hits, plus fewer wasted runs and a safer supply chain. Added and changed lines are highlighted.

# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
 
# IMPORTANT: DO NOT CHANGE THE NAME OF THIS WORKFLOW!
# The workflow named "Custard CI" triggers both custard-run.yaml and custard-run-dev.yaml.
# TODO: To sunset old tests remove `affected`, `lint`, and `test` jobs (only keep `region-tags`).
name: Custard CI
on:
  push:
    branches:
      - main
  pull_request:
  # schedule:
  #   # https://crontab.guru/#0_12_*_*_0
  #   - cron: 0 12 * * 0 # At 12:00 on Sunday
 
env:
  GO_VERSION: ^1.22.0
 
# TODO: remove all jobs except region-tags (should be tested by custard-run workflows)
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
 
jobs:
  affected:
    name: (legacy) Finding affected tests
    runs-on: latchkey-small
    timeout-minutes: 2
    outputs:
      nodejs-paths: ${{ steps.nodejs.outputs.paths }}
      nodejs-setups: ${{ steps.nodejs.outputs.setups }}
    steps:
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
        with:
          fetch-depth: 0
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
        with:
          repository: GoogleCloudPlatform/cloud-samples-tools
          ref: v0.3.2
          path: cloud-samples-tools
      - name: Create `bin` directory for cloud-samples-tools binaries
        run: mkdir bin
        working-directory: cloud-samples-tools
      - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5
        with:
          go-version: ${{ env.GO_VERSION }}
      - name: Build Custard (from cloud-samples-tools)
        run: go build -o ../bin -v ./...
        working-directory: cloud-samples-tools/custard
      - name: Get diffs
        run: git --no-pager diff --name-only HEAD origin/main | tee diffs.txt
      - name: Find Node.js affected packages
        id: nodejs
        run: |
          echo "paths=$(./cloud-samples-tools/bin/custard affected .github/config/nodejs.jsonc diffs.txt paths.txt)" >> $GITHUB_OUTPUT
          cat paths.txt
          echo "setups=$(./cloud-samples-tools/bin/custard setup-files .github/config/nodejs.jsonc paths.txt)" >> $GITHUB_OUTPUT
 
  lint:
    needs: affected
    runs-on: latchkey-small
    timeout-minutes: 5
    steps:
      - name: Checkout
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
      - name: Setup Node
        uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
        with:
          cache: 'npm'
          node-version: 20
      - run: npm install
      - name: Run lint
        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
        with:
          script: |
            const { execSync } = await import("node:child_process");
 
            const cmd = 'npx gts lint';
            const affected = ${{ needs.affected.outputs.nodejs-paths }};
            if (affected.length === 0) {
              console.log("No packages were affected, nothing to lint.")
            }
 
            let failed = [];
            for (const path of affected) {
              try {
                execSync(cmd, {cwd: path});
                console.log(`✅ [${path}]: ${cmd}`);
              } catch (e) {
                failed.push(path)
                console.log(`❌ [${path}]: ${cmd} (exit code ${e.status})`);
                core.error(e.message);
                console.log('--- stdout ---');
                console.log(e.stdout.toString("utf8"));
                console.log('--- stderr ---');
                console.log(e.stderr.toString("utf8"));
              }
            }
            console.log("=== Summary ===")
            console.log(`  Passed: ${affected.length - failed.length}`)
            console.log(`  Failed: ${failed.length}`)
            if (failed.length > 0) {
              core.setFailed(`Failed '${cmd}' on: ${failed.join(', ')}`)
            }
 
  region-tags:
    name: region tags
    runs-on: latchkey-small
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
        with:
          cache: 'npm'
          node-version: 20
      - run: ./.github/workflows/utils/region-tags-tests.sh
 
  test:
    if: github.event.pull_request && github.event.pull_request.head.repo.fork == false
    name: (legacy) test
    needs: affected
    runs-on: latchkey-small
    timeout-minutes: 120 # 2 hours hard limit
    permissions:
      id-token: write # needed for google-github-actions/auth
    strategy:
      fail-fast: false
      matrix:
        path: ${{ fromJson(github.event_name == 'pull_request' && needs.affected.outputs.nodejs-paths || '[]') }}
    env:
      GOOGLE_SAMPLES_PROJECT: long-door-651
      GOOGLE_SERVICE_ACCOUNT: kokoro-system-test@long-door-651.iam.gserviceaccount.com
      CI_SETUP: ${{ toJson(fromJson(needs.affected.outputs.nodejs-setups)[matrix.path])}}
    steps:
      - name: CI Setup
        run: echo "${{ env.CI_SETUP }}"
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
        with:
          cache: 'npm'
          node-version: ${{ fromJson(env.CI_SETUP).node-version }}
      - uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3
        id: auth
        with:
          project_id: ${{ env.GOOGLE_SAMPLES_PROJECT }}
          workload_identity_provider: projects/1046198160504/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider
          service_account: ${{ env.GOOGLE_SERVICE_ACCOUNT }}
          access_token_lifetime: 600s # 10 minutes
          token_format: 'id_token'
          id_token_audience: 'https://action.test/' # service must have this custom audience
          id_token_include_email: true
      - name: Export environment variables
        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
        id: vars
        with:
          script: |
            const { default: setupVars } = await import('${{ github.workspace }}/.github/scripts/setup-vars.js');
            const projectId = '${{ env.GOOGLE_SAMPLES_PROJECT }}';
            const setup = JSON.parse(process.env.CI_SETUP);
            const serviceAccount = '${{ env.GOOGLE_SERVICE_ACCOUNT }}';
            const idToken = '${{ steps.auth.outputs.id_token }}';
            return await setupVars({projectId, core, setup, serviceAccount, idToken})
      - uses: google-github-actions/get-secretmanager-secrets@e5bb06c2ca53b244f978d33348d18317a7f263ce # v2
        if: ${{ fromJson(steps.vars.outputs.result).secrets }}
        with:
          secrets: ${{ fromJson(steps.vars.outputs.result).secrets }}
          export_to_environment: true
      - name: Run tests for ${{ matrix.path }}
        run: |
          timeout ${{ fromJson(env.CI_SETUP).timeout-minutes }}m \
            make test dir=${{ matrix.path }}
 

What changed

What Latchkey heals here

This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:

This workflow runs 4 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow