Repository: Cantera/cantera
Pull request #1875: Add Zizmor linting and address findings
Author: login: bryanwweber
Labels: name: CI
Created: 2025-04-19T12:35:02Z | Merged: 2025-05-17T14:53:43Z

**Changes proposed in this pull request**

- **[CI] Address findings from zizmor**
- **[CI] Add zizmor job running on PRs and main**

**If applicable, provide an example illustrating new features this pull request is introducing**

The [zizmor](https://woodruffw.github.io/zizmor/) project is a static analysis tool for GitHub workflows. The main suggestion I've seen it make when I've used it is to avoid places where bad actors might inject arbitrary code using the Action template strings `${{ ... }}`. The other change it suggested here was to remove unnecessary job permissions.

My motivation to do this was the `changed-files-action` attack a few weeks ago. I don't think `zizmor` would have stopped that attack, but beefing up code quality/security seems worthwhile.

That said, I'm not convinced this is a good thing to add for `Cantera/cantera` itself. I've already [added it to the `pypi-packages`](https://github.com/Cantera/pypi-packages/blob/main/.github/workflows/zizmor.yml) repo, because we're publishing packages from there and I really don't want those to include malware :grimacing: Since we don't actually publish anything from this repo, I think it has a little lower impact, but it still seemed worth it to at least moot the idea with the associated changes. Thus, draft status and I'm happy to close it if we think it's too much noise.

**Checklist**

- [x] The pull request includes a clear description of this code change
- [x] Commit messages have short titles and reference relevant issues
- [x] Build passes (`scons build` & `scons test`) and unit tests address code coverage
- [x] Style & formatting of contributed code follows [contributing guidelines](https://github.com/Cantera/cantera/blob/main/CONTRIBUTING.md)
- [x] The pull request is ready for review



Diff:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000000..cc36e366224
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      # Check for updates to GitHub Actions every month
+      interval: "monthly"
+    groups:
+      actions:
+        patterns:
+          - "*"
diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index c06888fd439..c6fb4497304 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -6,6 +6,9 @@ on:
     branches:
       - main
 
+permissions:
+  contents: read
+
 jobs:
   linter:
     name: Lint checks
@@ -16,6 +19,7 @@ jobs:
         name: Checkout the repository
         with:
           fetch-depth: 100
+          persist-credentials: true
       - name: Setup Python
         uses: actions/setup-python@v5
         with:
@@ -25,7 +29,7 @@ jobs:
       - name: Get changed C++ files
         if: success() || failure()
         id: changed-cxx-files
-        uses: tj-actions/changed-files@v46
+        uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
         with:
           files: |
             **.h
@@ -35,7 +39,7 @@ jobs:
         if: (success() || failure()) && steps.changed-cxx-files.outputs.any_changed == 'true'
         run: |
           RETCODE=0
-          for file in ${{ steps.changed-cxx-files.outputs.all_changed_files }}; do
+          for file in ${ALL_CHANGED_FILES}; do
             RESULT=`perl -0777 -ne 'while(m/\n\n *\/\*!.*?\*\//gs){print "$&\n";}' $file`
             if [ "$RESULT" ]; then
               echo ------ ${file} ------
@@ -44,6 +48,8 @@ jobs:
             fi
           done
           exit $RETCODE
+        env:
+          ALL_CHANGED_FILES: ${{ steps.changed-cxx-files.outputs.all_changed_files }}
       - name: Whitespace errors
         if: success() || failure()
         run: |
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 68b7ea6201a..75fe827b2a6 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -9,13 +9,18 @@ on:
     branches:
       - main
       - testing
-      - "3.1"
+      # This workflow does not publish artifacts, so we can ignore the cache
+      # poisoning finding.
+      - "3.1" # zizmor: ignore[cache-poisoning]
   pull_request:
     # Build when a pull request targets main or the maintenance branch
     branches:
       - main
       - "3.1"
 
+permissions:
+  contents: read
+
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
   cancel-in-progress: true
@@ -32,6 +37,7 @@ jobs:
         name: Checkout the repository
         with:
           submodules: recursive
+          persist-credentials: false
       - name: Setup Python
         uses: actions/setup-python@v5
         with:
@@ -59,9 +65,9 @@ jobs:
           LIB_LAPACK=$(ldconfig -p | grep liblapack.so.3 | awk '{print $4}' | head -n 1)
           echo "LD_PRELOAD=$LIB_STDCXX:$LIB_OPENBLAS:$LIB_LAPACK" >> $GITHUB_ENV
       - name: Set up MATLAB
-        uses: matlab-actions/setup-matlab@v2
+        uses: matlab-actions/setup-matlab@718d4320188c73c86eb94ce76b553cbf89778487 # v2.5.0
       - name: Run tests
-        uses: matlab-actions/run-tests@v2
+        uses: matlab-actions/run-tests@f9fc3d8ca29fadef6227fa52884b144b9011fa2f # v2.1.1
         with:
           select-by-folder: /home/runner/work/cantera/cantera/test/matlab_experimental
 
@@ -96,6 +102,7 @@ jobs:
       name: Checkout the repository
       with:
         submodules: recursive
+        persist-credentials: false
     - name: Setup Python
       uses: actions/setup-python@v5
       with:
@@ -162,6 +169,7 @@ jobs:
       name: Checkout the repository
       with:
         submodules: recursive
+        persist-credentials: false
     - name: Setup Python
       uses: actions/setup-python@v5
       with:
@@ -221,6 +229,7 @@ jobs:
       name: Checkout the repository
       with:
         submodules: recursive
+        persist-credentials: false
     - name: Write dependencies to a file for caching
       run: |
         echo "scons ruamel.yaml numpy cython pandas pytest pytest-xdist pytest-github-actions-annotate-failures pint graphviz Jinja2" | tr " " "\n" > requirements.txt
@@ -260,9 +269,7 @@ jobs:
       run: scons test-clib-experimental --debug=time
       if: matrix.python-version == '3.12' && matrix.macos-version == 'macos-13'
     - name: Upload shared library
-      # Pin to 4.3.4 to resolve errors around only uploading symlinks.
-      # See https://github.com/actions/upload-artifact/issues/589
-      uses: actions/upload-artifact@v4.3.4
+      uses: actions/upload-artifact@v4
       if: matrix.python-version == '3.12' && matrix.macos-version == 'macos-13'
       with:
         path: build/lib/libcantera_shared.dylib
@@ -282,6 +289,7 @@ jobs:
       name: Checkout the repository
       with:
         submodules: recursive
+        persist-credentials: false
     - name: Setup python
       uses: actions/setup-python@v5
       with:
@@ -334,9 +342,9 @@ jobs:
         LIB_LAPACK=$(ldconfig -p | grep liblapack.so.3 | awk '{print $4}' | head -n 1)
         echo "LD_PRELOAD=$LIB_STDCXX:$LIB_OPENBLAS:$LIB_LAPACK" >> $GITHUB_ENV
     - name: Set up MATLAB
-      uses: matlab-actions/setup-matlab@v2
+      uses: matlab-actions/setup-matlab@718d4320188c73c86eb94ce76b553cbf89778487 # v2.5.0
     - name: Test the MATLAB interface and generate coverage report
-      uses: matlab-actions/run-command@v1
+      uses: matlab-actions/run-command@67f012f1ee4bc1627a7a95a0ccdeec745c9f6f36 # v2.2.1
       env:
         CANTERA_ROOT: /home/runner/work/cantera/cantera
         CANTERA_DATA: /home/runner/work/cantera/cantera/data
@@ -375,7 +383,7 @@ jobs:
         path: test/matlab_experimental/matlabCoverage*
         retention-days: 5
     - name: Upload Coverage to Codecov
-      uses: codecov/codecov-action@v5
+      uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
       with:
         verbose: true
         files: ./coverage.xml,./build/pycov.xml,./interfaces/dotnet/coverage.cobertura.xml,./test/matlab_experimental/matlabCoverage.xml
@@ -393,8 +401,9 @@ jobs:
         name: Checkout the repository
         with:
           submodules: recursive
+          persist-credentials: false
       - name: Set up micromamba
-        uses: mamba-org/setup-micromamba@v1
+        uses: mamba-org/setup-micromamba@0dea6379afdaffa5d528b3d1dabc45da37f443fc # v2.0.4
         with:
           environment-name: test-env
           create-args: >-
@@ -445,30 +454,32 @@ jobs:
         if: failure() || success()
       - name: Determine whether to deploy
         id: deploy-conf
-        if: ${{ github.event_name == 'push' && github.repository_owner == 'Cantera' }}
+        if: github.event_name == 'push' && github.repository_owner == 'Cantera'
         run: |
-          if [[ ${{ github.ref }} =~ ^refs\/heads\/([0-9]+\.[0-9]+)$ ]]; then
+          if [[ ${GITHUB_REF} =~ ^refs\/heads\/([0-9]+\.[0-9]+)$ ]]; then
               echo "match=true" >> $GITHUB_OUTPUT
               echo "rsync_dest=cantera/${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
               echo "commit=true" >> $GITHUB_OUTPUT
               echo "git_dest=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
               echo "git_branch=staging" >> $GITHUB_OUTPUT
-          elif [[ ${{ github.ref }} == "refs/heads/main" ]]; then
+          elif [[ ${GITHUB_REF} == "refs/heads/main" ]]; then
               echo "match=true" >> $GITHUB_OUTPUT
               echo "rsync_dest=cantera/dev" >> $GITHUB_OUTPUT
-          elif [[ ${{ github.ref }} == "refs/heads/testing" ]]; then
+          elif [[ ${GITHUB_REF} == "refs/heads/testing" ]]; then
               echo "match=true" >> $GITHUB_OUTPUT
               echo "rsync_dest=testing.cantera.org/dev" >> $GITHUB_OUTPUT
               echo "commit=true" >> $GITHUB_OUTPUT
               echo "git_dest=testing" >> $GITHUB_OUTPUT
               echo "git_branch=testing" >> $GITHUB_OUTPUT
           fi
+        env:
+          GITHUB_REF: ${{ github.ref }}
       # The known_hosts key is generated with `ssh-keygen -F cantera.org` from a
       # machine that has previously logged in to cantera.org and trusts
       # that it logged in to the right machine
       - name: Set up SSH key and host for deploy
         if: steps.deploy-conf.outputs.match == 'true'
-        uses: shimataro/ssh-key-action@v2
+        uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 # v2.7.0
         with:
           key: ${{ secrets.CTDEPLOY_KEY }}
           known_hosts: ${{ secrets.CTDEPLOY_HOST }}
@@ -551,6 +562,8 @@ jobs:
       # folder, so no need to do a recursive checkout
       - uses: actions/checkout@v4
         name: Checkout the repository
+        with:
+          persist-credentials: false
       - name: Setup Python
         uses: actions/setup-python@v5
         with:
@@ -634,8 +647,9 @@ jobs:
         name: Checkout the repository
         with:
           submodules: recursive
+          persist-credentials: false
       - name: Set up micromamba
-        uses: mamba-org/setup-micromamba@v1
+        uses: mamba-org/setup-micromamba@0dea6379afdaffa5d528b3d1dabc45da37f443fc # v2.0.4
         with:
           environment-name: test-env
           # See https://github.com/conda-forge/boost-cpp-feedstock/issues/41 for why we
@@ -700,8 +714,9 @@ jobs:
       name: Checkout the repository
       with:
         submodules: recursive
+        persist-credentials: false
     - name: Set up micromamba
-      uses: mamba-org/setup-micromamba@v1
+      uses: mamba-org/setup-micromamba@0dea6379afdaffa5d528b3d1dabc45da37f443fc # v2.0.4
       with:
         environment-name: test-env
         # See https://github.com/conda-forge/boost-cpp-feedstock/issues/41 for why we
@@ -784,6 +799,7 @@ jobs:
         name: Checkout the repository
         with:
           submodules: recursive
+          persist-credentials: false
       - name: Set Up Python
         uses: actions/setup-python@v5
         with:
@@ -847,6 +863,7 @@ jobs:
       name: Checkout the repository
       with:
         submodules: recursive
+        persist-credentials: false
     - name: Setup Python
       uses: actions/setup-python@v5
       with:
@@ -886,6 +903,7 @@ jobs:
       name: Checkout the repository
       with:
         submodules: recursive
+        persist-credentials: false
     - name: Set Up Python
       uses: actions/setup-python@v5
       with:
@@ -902,7 +920,7 @@ jobs:
         path: ${{env.BOOST_ROOT}}
         key: boost-187-win
     - name: Set up MinGW
-      uses: egor-tensin/setup-mingw@v2
+      uses: egor-tensin/setup-mingw@84c781b557efd538dec66bde06988d81cd3138cf # v2.2.0
       with:
         platform: x64
         static: false
@@ -955,6 +973,8 @@ jobs:
     steps:
     - uses: actions/checkout@v4
       name: Checkout the repository
+      with:
+        persist-credentials: false
     - name: Override default Python (Windows)
       # Other operating systems use default system Python 3
       uses: actions/setup-python@v5
@@ -966,7 +986,7 @@ jobs:
       # Install Python dependencies, which are used by 'dotnet build'
       run: python3 -m pip install ruamel.yaml Jinja2 typing-extensions
     - name: Install library dependencies with micromamba (Windows)
-      uses: mamba-org/setup-micromamba@v1
+      uses: mamba-org/setup-micromamba@0dea6379afdaffa5d528b3d1dabc45da37f443fc # v2.0.4
       with:
         environment-name: test-env
         # fmt needs to match the version of the windows-2022 runner selected to upload
diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml
index d49e496e32e..85bcd336200 100644
--- a/.github/workflows/packaging.yml
+++ b/.github/workflows/packaging.yml
@@ -17,6 +17,14 @@ on:
     # Run when main is pushed to
     branches:
       - main
+  pull_request:
+    # Run on pull requests when this file is modified
+    branches:
+      - main
+    paths:
+      - .github/workflows/packaging.yml
+
+permissions: {}
 
 jobs:
   build-packages:
@@ -24,24 +32,31 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: Set up the job
+        id: setup
         run: |
-          if [ -n "${{ github.event.inputs.outgoing_ref }}" ]; then
-            echo "REF=${{ github.event.inputs.outgoing_ref }}" >> $GITHUB_ENV
+          if [ -n "${OUTGOING_REF}" ]; then
+            echo "REF=${OUTGOING_REF}" >> $GITHUB_OUTPUT
           else
-            echo "REF=${{ github.ref }}" >> $GITHUB_ENV
+            echo "REF=${GITHUB_REF}" >> $GITHUB_OUTPUT
           fi
-          if [ -n "${{ github.event.inputs.upload_to_pypi }}" ]; then
-            echo "UPLOAD_TO_PYPI=${{ github.event.inputs.upload_to_pypi }}" >> $GITHUB_ENV
-          elif [[ "${{ github.ref }}" == refs/tags* ]]; then
-            echo "UPLOAD_TO_PYPI=true" >> $GITHUB_ENV
+          if [ -n "${UPLOAD_TO_PYPI}" ]; then
+            echo "UPLOAD_TO_PYPI=${UPLOAD_TO_PYPI}" >> $GITHUB_OUTPUT
+          elif [[ "${GITHUB_REF}" == refs/tags* ]]; then
+            echo "UPLOAD_TO_PYPI=true" >> $GITHUB_OUTPUT
           else
-            echo "UPLOAD_TO_PYPI=false" >> $GITHUB_ENV
+            echo "UPLOAD_TO_PYPI=false" >> $GITHUB_OUTPUT
           fi
+        env:
+          OUTGOING_REF: ${{ github.event.inputs.outgoing_ref }}
+          UPLOAD_TO_PYPI: ${{ github.event.inputs.upload_to_pypi }}
+          GITHUB_REF: ${{ github.ref }}
       - name: Trigger PyPI/Wheel builds
         run: >
           gh workflow run -R cantera/pypi-packages
           python-package.yml
-          -f incoming_ref=${{ env.REF }}
-          -f upload=${{ env.UPLOAD_TO_PYPI }}
+          -f incoming_ref=${REF}
+          -f upload=${UPLOAD_TO_PYPI}
         env:
           GITHUB_TOKEN: ${{ secrets.PYPI_PACKAGE_PAT }}
+          REF: ${{ steps.setup.outputs.REF }}
+          UPLOAD_TO_PYPI: ${{ steps.setup.outputs.UPLOAD_TO_PYPI }}
diff --git a/.github/workflows/post-merge-tests.yml b/.github/workflows/post-merge-tests.yml
index 71e66cb2157..651c12bf920 100644
--- a/.github/workflows/post-merge-tests.yml
+++ b/.github/workflows/post-merge-tests.yml
@@ -17,6 +17,9 @@ on:
     paths:
       - .github/workflows/post-merge-tests.yml
 
+permissions:
+  contents: read
+
 env:
   PIP_EXTRA_INDEX_URL: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
   PIP_ONLY_BINARY: ":all:"
@@ -31,6 +34,7 @@ jobs:
       name: Checkout the repository
       with:
         submodules: recursive
+        persist-credentials: false
     - name: Setup Python
       uses: actions/setup-python@v5
       with:
@@ -82,6 +86,7 @@ jobs:
       name: Checkout the repository
       with:
         submodules: recursive
+        persist-credentials: false
     - name: Install Apt dependencies
       # Use packages from ubuntu image when possible
       run: |
@@ -130,6 +135,8 @@ jobs:
         git config --global --add safe.directory /__w/cantera/cantera
     - uses: actions/checkout@v4
       name: Checkout the repository
+      with:
+        persist-credentials: false
     - name: Install dependencies
       # Use packages from Fedora
       run: |
@@ -171,6 +178,7 @@ jobs:
       name: Checkout the repository
       with:
         submodules: recursive
+        persist-credentials: false
     - name: Setup Python
       uses: actions/setup-python@v5
       with:
@@ -214,6 +222,7 @@ jobs:
       name: Checkout the repository
       with:
         submodules: recursive
+        persist-credentials: false
     # NumPy is installed here for the Python custom rate tests
     - name: Install Brew dependencies
       run: brew install --display-times boost libomp hdf5 python numpy
diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml
new file mode 100644
index 00000000000..3660361e4e1
--- /dev/null
+++ b/.github/workflows/zizmor.yml
@@ -0,0 +1,33 @@
+name: GitHub Actions Security Analysis with zizmor 🌈
+
+on:
+  push:
+    branches: ["main"]
+  pull_request:
+    branches: ["**"]
+
+jobs:
+  zizmor:
+    name: zizmor latest via PyPI
+    runs-on: ubuntu-latest
+    permissions:
+      security-events: write
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          persist-credentials: false
+
+      - name: Install the latest version of uv
+        uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1
+
+      - name: Run zizmor 🌈
+        run: uvx zizmor --format=sarif . > results.sarif
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Upload SARIF file
+        uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
+        with:
+          sarif_file: results.sarif
+          category: zizmor
diff --git a/doc/sphinx/develop/continuous-integration.md b/doc/sphinx/develop/continuous-integration.md
new file mode 100644
index 00000000000..79a876d126b
--- /dev/null
+++ b/doc/sphinx/develop/continuous-integration.md
@@ -0,0 +1,74 @@
+# Continuous Integration
+
+Cantera uses [GitHub Actions](https://docs.github.com/en/actions) to run a suite of tests and code quality checks to help avoid regressions and make sure Cantera's code has a consistent style. Many of these checks run automatically on each push to a pull request and a few run only when a pull request is merged into the `main` branch. This section describes how the workflows are set up and requirements for adding or changing checks.
+
+## Existing Workflows
+
+All the workflows that run on GitHub Actions are in the `.github/workflows` folder:
+
+- `linters.yml`: Has actions that check that code is formatted correctly. Runs on every push for a pull request, and every commit to `main`.
+- `main.yml`: Has actions that run the test suite to avoid unexpected changes in behavior. Runs on every push for a pull request, and every commit to `main`.
+- `packaging.yml`: Has actions that check our packaging workflows. Runs on every commit to `main`.
+- `post-merge-tests.yml`: Has actions that take a long time to run, or that run the test suite on less commonly-used platforms. Runs on every commit to `main`.
+- `zizmor.yml`: Has an action to run [zizmor](https://woodruffw.github.io/zizmor/), a program that statically checks workflow files for best-practices. Runs on every push for a pull request, and every commit to `main`.
+
+## Modifying or Adding Workflows
+
+Workflows can be modified to include additional jobs or modify the behavior of existing jobs. Workflows can also be added if another category of check becomes useful. The best bet is to follow the existing patterns in the workflow files while making updates as this will help make sure that modified jobs work properly. Keep in mind the following tips as well:
+
+1. Steps in a job that use a 3rd party action with the `uses` keyword must pin the action to a SHA hash. The line should be formatted as:
+
+   ```yaml
+   - name: Step
+     uses: <organization>/<repository>@<40 character commit hash> # <version tag>
+   ...
+   ```
+
+   Pinning actions in this way makes sure that our actions are always using the same version of a the 3rd party code that is expected.
+
+   1. Steps developed by GitHub in the `actions` organization do not need to be pinned to a SHA hash but they can be if desired. If a SHA has is not used, steps from the `actions` repository _must_ be pinned to a tag or branch that matches the regex pattern `^v*$`.
+   2. The versions of steps are kept up-to-date by Dependabot, an automated service that is configured to run monthly. Dependabot will update the hash and the version tag whenever updates for an action are available.
+2. When checking out a repository in a job, please set `persist-credentials: false` in the `with` block. For example:
+
+   ```yaml
+   - name: Checkout the repository
+     uses: actions/checkout@v4
+     with:
+       persist-credentials: false
+       <other options>
+    ```
+3. Workflows and jobs should be configured with the [minimal permissions](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token) necessary to complete their steps. In general, this means you should add the following configuration at the top-level of every workflow:
+
+   ```yaml
+   permissions: {}
+   ```
+
+   If jobs need additional permissions, they should be configured per-job to make sure the extra permissions are not given to other jobs in the workflow as in the following example:
+
+   ```yaml
+   jobs:
+     job-name:
+       permissions:
+         contents: read
+         ...
+   ```
+4. Do not use template substitution with `${{ }}` directly in scripts. Doing so can result in unexpected code being executed if the template string contains something that Bash can interpret. Instead, assign the template to an environment variable and use the variable in the script:
+
+   ```yaml
+   # DO NOT DO THIS
+   - run: echo "${{ github.ref }}"
+
+   # DO THIS INSTEAD
+   - run: echo "$GH_REF"
+     env:
+       GH_REF: "${{ github.ref }}"
+   ```
+
+   For more information, see the [Zizmor documentation](https://woodruffw.github.io/zizmor/audits/#template-injection) about template injection.
+5. You can run Zizmor locally to statically check changes in workflow files with the following command:
+
+   ```bash
+   GH_TOKEN=$(gh auth token) zizmor .github/workflows
+   ```
+
+   This uses the [`gh` CLI program](https://cli.github.com/manual/) to get a token to run online checks with Zizmor.
diff --git a/doc/sphinx/develop/index.md b/doc/sphinx/develop/index.md
index e38e040c556..4aeecbd835e 100644
--- a/doc/sphinx/develop/index.md
+++ b/doc/sphinx/develop/index.md
@@ -54,6 +54,7 @@ reactor-integration
 - [](running-tests)
 - [](writing-examples)
 - [](doc-formatting)
+- [](continuous-integration)
 
 ```{toctree}
 :caption: Adding New Features to Cantera
@@ -66,6 +67,7 @@ writing-tests
 running-tests
 writing-examples
 doc-formatting
+continuous-integration
 ```
 
 (sec-distributing)=


Reviews:
user: login: ischoegl
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-24T15:42:08Z
user: login: ischoegl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T13:36:50Z
user: login: bryanwweber
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T15:19:28Z
user: login: ischoegl
body: Thanks, @bryanwweber - approving again.
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-28T15:08:24Z
user: login: speth
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-28T17:58:42Z
user: login: bryanwweber
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T18:06:31Z
user: login: bryanwweber
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-04T11:46:38Z
user: login: bryanwweber
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-04T11:46:49Z
user: login: ischoegl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-04T12:24:30Z
user: login: speth
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-15T01:56:18Z
user: login: ischoegl
body: Thanks @bryanwweber! ... the fix addresses the comment.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-17T14:51:12Z

Review comments:
diff_hunk: @@ -0,0 +1,7 @@
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      # Check for updates to GitHub Actions every week
path: .github/dependabot.yml
user: login: ischoegl
body: Week or month?
created_at: 2025-04-27T13:18:27Z
author_association: MEMBER
diff_hunk: @@ -25,7 +29,7 @@ jobs:
       - name: Get changed C++ files
         if: success() || failure()
         id: changed-cxx-files
-        uses: tj-actions/changed-files@v46
+        uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
path: .github/workflows/linters.yml
user: login: ischoegl
body: Would leaving the trailing comments start to be misleading as tags are updated? Or is dependabot smart enough to update?
created_at: 2025-04-27T13:35:36Z
author_association: MEMBER
diff_hunk: @@ -25,7 +29,7 @@ jobs:
       - name: Get changed C++ files
         if: success() || failure()
         id: changed-cxx-files
-        uses: tj-actions/changed-files@v46
+        uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
path: .github/workflows/linters.yml
user: login: bryanwweber
body: Dependabot is, fortunately, smart enough to update the comments 😁
created_at: 2025-04-27T15:19:28Z
author_association: MEMBER
diff_hunk: @@ -44,6 +48,8 @@ jobs:
             fi
           done
           exit $RETCODE
+        env:
+          ALL_CHANGED_FILES: ${{ steps.changed-cxx-files.outputs.all_changed_files }}
path: .github/workflows/linters.yml
user: login: speth
body: I don't quite understand the potential attack vector here, but if the need is to not use the `${{ }}` style substitutions in scripts, then can we please document that in the developer docs (`doc/sphinx/develop/...`). I guess we might need a new page on CI stuff.
created_at: 2025-04-28T17:50:24Z
author_association: MEMBER
diff_hunk: @@ -59,9 +65,9 @@ jobs:
           LIB_LAPACK=$(ldconfig -p | grep liblapack.so.3 | awk '{print $4}' | head -n 1)
           echo "LD_PRELOAD=$LIB_STDCXX:$LIB_OPENBLAS:$LIB_LAPACK" >> $GITHUB_ENV
       - name: Set up MATLAB
-        uses: matlab-actions/setup-matlab@v2
+        uses: matlab-actions/setup-matlab@718d4320188c73c86eb94ce76b553cbf89778487 # v2.5.0
path: .github/workflows/main.yml
user: login: speth
body: Likewise, the need to specify particular hashes for 3rd party actions (I guess all bets are off if the core GitHub actions are compromised...), and how these get updated (can't say I'm that excited for the dependabot-driven churn, but oh well).
created_at: 2025-04-28T17:56:15Z
author_association: MEMBER
diff_hunk: @@ -96,6 +102,7 @@ jobs:
       name: Checkout the repository
       with:
         submodules: recursive
+        persist-credentials: false
path: .github/workflows/main.yml
user: login: speth
body: Is there any way to make this a default? Otherwise, also needs to be documented.
created_at: 2025-04-28T17:57:04Z
author_association: MEMBER
diff_hunk: @@ -44,6 +48,8 @@ jobs:
             fi
           done
           exit $RETCODE
+        env:
+          ALL_CHANGED_FILES: ${{ steps.changed-cxx-files.outputs.all_changed_files }}
path: .github/workflows/linters.yml
user: login: bryanwweber
body: My understanding is that direct template substitutions in running code can lead to arbitrary code execution. For example, a malicious actor could inject a string like `value$(rm -rf /)value`. If this is templated directly into the script, it will actually be executed. However, putting into an environment variable this way ensures that string escaping is done and the code won't be injected. 

I'm not familiar with all the weird ways that bash can find code to execute, so this might not be a particularly relevant example, but hopefully you get the gist. The problem is not the templated string, but rather expanding the template directly in the script context. 

Yes, I'll definitely document it! 
created_at: 2025-04-28T18:06:31Z
author_association: MEMBER
diff_hunk: @@ -59,9 +65,9 @@ jobs:
           LIB_LAPACK=$(ldconfig -p | grep liblapack.so.3 | awk '{print $4}' | head -n 1)
           echo "LD_PRELOAD=$LIB_STDCXX:$LIB_OPENBLAS:$LIB_LAPACK" >> $GITHUB_ENV
       - name: Set up MATLAB
-        uses: matlab-actions/setup-matlab@v2
+        uses: matlab-actions/setup-matlab@718d4320188c73c86eb94ce76b553cbf89778487 # v2.5.0
path: .github/workflows/main.yml
user: login: bryanwweber
body: Couple of thoughts:
1. We don't have to pin to hashes, we can configure zizmor to accept tags instead. But since the tj-actions thing, I'm inclined to just use hashes and let dependabot keep them updated
2. Dependabot is configured to run monthly and I'll set it to group updates into a single PR. Hopefully that will minimize the additional churn/overhead.
created_at: 2025-05-04T11:46:38Z
author_association: MEMBER
diff_hunk: @@ -96,6 +102,7 @@ jobs:
       name: Checkout the repository
       with:
         submodules: recursive
+        persist-credentials: false
path: .github/workflows/main.yml
user: login: bryanwweber
body: No, there isn't as far as I know. I'll document it.
created_at: 2025-05-04T11:46:49Z
author_association: MEMBER
diff_hunk: @@ -59,9 +65,9 @@ jobs:
           LIB_LAPACK=$(ldconfig -p | grep liblapack.so.3 | awk '{print $4}' | head -n 1)
           echo "LD_PRELOAD=$LIB_STDCXX:$LIB_OPENBLAS:$LIB_LAPACK" >> $GITHUB_ENV
       - name: Set up MATLAB
-        uses: matlab-actions/setup-matlab@v2
+        uses: matlab-actions/setup-matlab@718d4320188c73c86eb94ce76b553cbf89778487 # v2.5.0
path: .github/workflows/main.yml
user: login: ischoegl
body: While it’s producing churn, I consider it relatively minor.
created_at: 2025-05-04T12:24:30Z
author_association: MEMBER

PR comments:
user: login: github-advanced-security[bot]
created_at: 2025-04-19T12:35:47Z
body: This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on [this overview](/Cantera/cantera/security/code-scanning?query=pr%3A1875+is%3Aopen). Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out [the documentation](https://docs.github.com/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning). 
author_association: NONE
user: login: codecov[bot]
created_at: 2025-04-19T12:57:45Z
body: ## [Codecov](https://app.codecov.io/gh/Cantera/cantera/pull/1875?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Cantera) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 74.13%. Comparing base [(`0eb7f94`)](https://app.codecov.io/gh/Cantera/cantera/commit/0eb7f942af42fb67c6d0bc46ccb3ab61e2a822b3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Cantera) to head [(`6297267`)](https://app.codecov.io/gh/Cantera/cantera/commit/6297267603bee9c2a5cc7b757b09a039791f0548?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Cantera).
> Report is 22 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1875      +/-   ##
==========================================
- Coverage   74.33%   74.13%   -0.21%     
==========================================
  Files         443      443              
  Lines       55403    55400       -3     
  Branches     9108     9107       -1     
==========================================
- Hits        41183    41069     -114     
- Misses      11122    11241     +119     
+ Partials     3098     3090       -8     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Cantera/cantera/pull/1875?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Cantera).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Cantera).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: ischoegl
created_at: 2025-04-24T13:42:48Z
body: @bryanwweber  … I’d be :+1: with this change. The post-merge test failures are unrelated as far as I can tell, but there’s one packaging test that could be addressed?
author_association: MEMBER
user: login: bryanwweber
created_at: 2025-04-24T14:31:18Z
body: Thanks @ischoegl! The packaging test is because that workflow running on a PR from my work doesn't have permission to access the token to start the workflow in the other repository. 
author_association: MEMBER
user: login: speth
created_at: 2025-04-28T17:46:06Z
body: Why are there no CI runs showing up here any more?
author_association: MEMBER
user: login: speth
created_at: 2025-05-15T01:57:55Z
body: Ah, I just figured out what the failing `CI / Build docs` was complaining about. The relevant output is:
```
/home/runner/work/cantera/cantera/build/doc/sphinx/develop/continuous-integration.md: WARNING: document isn't included in any toctree
```
The issue is that in `doc/sphinx/develop/index.md`, the `continuous-integration` file needs to be listed not only in the bulleted list but also in the hidden TOC.
author_association: MEMBER
user: login: bryanwweber
created_at: 2025-05-17T11:34:52Z
body: Thanks @speth! Hopefully I just fixed that, but of course it dismissed your review :smile: 
author_association: MEMBER