> ## Documentation Index
> Fetch the complete documentation index at: https://site.aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# GitHub Actions Dynamic Matrix

> Guide to configuring dynamic matrices in GitHub Actions using bash scripts and JSON arrays for flexible job handling.

export const BlogPost = ({title, date, authors, tags, image, children}) => {
  const tagList = tags ? tags.split(", ").filter(Boolean) : [];
  const tagSlug = t => t.toLowerCase().replace(/&/g, "").replace(/\+/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
  const formattedDate = date ? new Date(date + "T00:00:00").toLocaleDateString("en-US", {
    year: "numeric",
    month: "long",
    day: "numeric"
  }) : "";
  return <section className="w-full flex justify-center px-4 py-12 md:py-16">
      <div style={{
    maxWidth: "800px",
    width: "100%"
  }}>
        {image && (typeof image === "string" ? <img noZoom src={image} alt={title} className="w-full rounded-xl mb-8" style={{
    maxHeight: "400px",
    objectFit: "cover"
  }} /> : <div className="blog-post-hero-image">{image}</div>)}
        <h1 className="text-3xl md:text-4xl font-bold text-zinc-900 dark:text-white">
          {title}
        </h1>
        <div className="flex flex-wrap items-center gap-3 mt-4 text-sm text-zinc-500 dark:text-zinc-400">
          {authors && <span>{authors}</span>}
          {authors && formattedDate && <span>·</span>}
          {formattedDate && <span>{formattedDate}</span>}
        </div>
        {tagList.length > 0 && <div className="flex flex-wrap gap-2 mt-3">
            {tagList.map(tag => <a key={tag} href={"/blog/tags/" + tagSlug(tag)} className="px-2 py-0.5 rounded-full text-xs bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-blue-100 dark:hover:bg-blue-900/40 hover:text-blue-700 dark:hover:text-blue-300 transition">
                {tag}
              </a>)}
          </div>}
        <hr className="my-8 border-zinc-200 dark:border-zinc-700" />
        <div className="prose dark:prose-invert max-w-none">{children}</div>
      </div>
    </section>;
};

export const MarketingPage = () => <div className="marketing-page-marker" style={{
  display: "none"
}} />;

export const Section = ({children, className = "", gray = false, dark = false, id}) => <section id={id} className={`w-full flex justify-center px-4 py-16 md:py-24 ${gray ? "bg-gray-50 dark:bg-zinc-900" : dark ? "bg-zinc-900 dark:bg-zinc-950" : ""} ${className}`}>
    <div className="w-full" style={{
  maxWidth: "1140px"
}}>
      {children}
    </div>
  </section>;

<MarketingPage />

<BlogPost title="GitHub Actions Dynamic Matrix" date="2022-10-28" authors="Alex Eagle" tags="CI/CD, Bazel">
  <img noZoom src="https://mintcdn.com/aspectbuild/x1L7Iep716jCyJVo/images/blog/stock/network-mesh.jpg?fit=max&auto=format&n=x1L7Iep716jCyJVo&q=85&s=0911ebc13499261ebce9979ce56e500d" alt="" className="blog-post-cover" width="800" height="533" data-path="images/blog/stock/network-mesh.jpg" />

  We needed to configure GitHub actions to run a matrix of jobs, but the values weren't static. For example:

  * one job requires a secret auth token, thus it can't be run on untrusted code from pull requests (for example, a private NPM registry is used, or Bazel's Remote Build Execution)
  * we might want to read a line from a config file like `.bazelversion` and use that value in a matrix dimension

  GitHub actions themselves [don't document this at all](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs). Maybe they should!

  The answer here is fantastic, but really long:
  [https://stackoverflow.com/questions/65384420/how-to-make-a-github-action-matrix-element-conditional](https://stackoverflow.com/questions/65384420/how-to-make-a-github-action-matrix-element-conditional)

  It's also a bit out-of-date since GitHub is deprecating the syntax it uses:
  [https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/](https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/)

  And it uses a separate JSON file which felt like overkill for my case. Here's a quicker recipe:

  ## Define one or more "matrix-prep" jobs

  Each of these contributes the values needed by one dimension of the matrix. It just runs bash one-liners, then the results are aggregated into a JSON array. For example to make a value conditional on having some secret available:

  ```yaml theme={null}
  jobs:
    matrix-prep-config:
      # Prepares the 'config' axis of the test matrix
      runs-on: ubuntu-latest
      env:
        # Grab a secret from the GitHub environment, will be empty string if the secret isn't
        # visible such as for untrusted code in a Pull Request
        ENGFLOW_PRIVATE_KEY: ${{ secrets.ENGFLOW_PRIVATE_KEY }}
      steps:
        - id: local
          run: echo "config=local" >> $GITHUB_OUTPUT
        - id: rbe
          run: echo "config=rbe" >> $GITHUB_OUTPUT
          # Don't run RBE if there are no EngFlow creds which is the case on forks
          if: ${{ env.ENGFLOW_PRIVATE_KEY != '' }}
      outputs:
        # Result will look like '["local", "rbe"]' if the secret was present or
        # '["local"]' otherwise
        configs: ${{ toJSON(steps.*.outputs.config) }}
  ```

  or another example where we need to read a file from the repo to find the values:

  ```yaml theme={null}
  jobs:
    matrix-prep-bazelversion:
      # Prepares the 'bazelversion' axis of the test matrix
      runs-on: ubuntu-latest
      steps:
        # Need the repo checked out in order to read the file
        - uses: actions/checkout@v3
        - id: bazel_6
          run: echo "bazelversion=$(head -n 1 .bazelversion)" >> $GITHUB_OUTPUT
        - id: bazel_5
          run: echo "bazelversion=5.3.2" >> $GITHUB_OUTPUT
      outputs:
        # Will look like '["6.0.0rc1", "5.3.2"]'
        bazelversions: ${{ toJSON(steps.*.outputs.bazelversion) }}
  ```

  ## Use that JSON value in the matrix definition

  We'll use `needs` to wait for the above jobs to complete and to read the values produced.

  ```yaml theme={null}
  jobs:
    [...]
    test:
      runs-on: ubuntu-latest

      needs:
        - matrix-prep-config
        - matrix-prep-bazelversion

      strategy:
        matrix:
          # Reads the value saved by "outputs" of the jobs above
          config: ${{ fromJSON(needs.matrix-prep-config.outputs.configs) }}
          bazelversion: ${{ fromJSON(needs.matrix-prep-bazelversion.outputs.bazelversions) }}

          # Another dimension with static values
          folder:
            - "."
            - "e2e/bzlmod"
            - "e2e/copy_to_directory"

          # Exclusions work like normal
          exclude:
            - config: rbe
              bazelversion: 5.3.2
              folder: e2e/bzlmod
  ```

  Here's the full example: [https://github.com/aspect-build/bazel-lib/blob/0c8ef86684d5a3335bb5e911a51d64e5fab39f9b/.github/workflows/ci.yaml](https://github.com/aspect-build/bazel-lib/blob/0c8ef86684d5a3335bb5e911a51d64e5fab39f9b/.github/workflows/ci.yaml)
</BlogPost>
