Skip to content
Latchkey

MMPM Build Pipeline workflow (Bee-Mar/mmpm)

The MMPM Build Pipeline workflow from Bee-Mar/mmpm, explained and optimized by Latchkey.

F

CI health: F - at risk

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

Grade your own workflow free or run it on Latchkey →
Source: Bee-Mar/mmpm.github/workflows/workflow.yamlLicense MITView source

What it does

This is the MMPM Build Pipeline workflow from the Bee-Mar/mmpm repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.

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

The workflow

workflow (.yml)
name: MMPM Build Pipeline
on:
  push:
    branches:
      - "*"
    tags:
      - "*"
  pull_request:
    branches:
      - "*"
  release:
    types: [created]

env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
  mmpm-cli:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version:
          - "3.10"
          - "3.11"
          - "3.12"
          - "3.13"
          - "3.14"
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Install uv
        uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
        with:
          version: 0.8.19
          python-version: ${{ matrix.python-version }}
          enable-cache: true

      - name: Python Dependencies
        run: |
          uv sync --locked --all-groups

      - name: Python Tests
        run: |
          uv run mypy mmpm
          uv run ruff check mmpm tests
          uv run coverage run -m pytest
          uv run coverage report

      - name: Python Build
        run: |
          uv sync --locked --all-groups
          uv run ruff check mmpm tests
          uv build

      - name: Install Test
        run: |
          options=("version" "--help" "completion --help" "db --help" "guided-setup --help" "install --help" "list --help" "logs --help" "mm-ctl --help" "mm-pkg --help" "open --help" "remove --help" "search --help" "show --help" "ui --help" "update --help" "upgrade --help" "version --help")

          for option in "${options[@]}"; do ${{ github.workspace }}/.venv/bin/mmpm $option; done

      - name: Install Nix
        uses: nixbuild/nix-quick-install-action@v30
        with:
          nix_conf: |
            keep-env-derivations = true
            keep-outputs = true

      - name: Restore and save Nix store
        uses: nix-community/cache-nix-action@v7
        with:
          primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.lock') }} # restore and save a cache using this key
          restore-prefixes-first-match: nix-${{ runner.os }}- # if there's no cache hit, restore a cache by this prefix
          # collect garbage until the Nix store size (in bytes) is at most this number
          # before trying to save a new cache
          # 1G = 1073741824
          gc-max-store-size-linux: 1G
          purge: true # do purge caches
          purge-prefixes: nix-${{ runner.os }}- # purge all versions of the cache
          purge-created: 0 # created more than this number of seconds ago
          # or last accessed this duration (ISO 8601 duration format)
          # before the start of the `Post Restore and save Nix store` phase
          purge-last-accessed: P1DT12H
          purge-primary-key: never # except any version with the key that is the same as the `primary-key`

      - name: Nix Build
        run: nix build

      - name: "Test MMPM download"
        run: |
          python -m pip install mmpm
          mmpm_exc="${{ github.workspace }}/.venv/bin/mmpm"

          mmpm list --all # just to see the stdout...doing it twice is a bit wasteful, but it's fine
          mmpm list --all > database.dump

          exit_code=0

          if grep -q "breaking change on the MagicMirror 3rd Party Modules wiki" database.dump; then
              exit_code=1
          fi

          rm database.dump
          exit $exit_code

  mmpm-ui:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: ["22", "24"]
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Cache Node Modules
        uses: actions/cache@v5
        with:
          path: |
            './ui/node_modules'
          key: angular-deps-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            angular-deps-${{ hashFiles('**/package-lock.json') }}

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v6
        with:
          node-version: ${{ matrix.node-version }}

      - name: Angular Dependencies
        run: |
          cd ui && npm install --legacy-peer-deps && cd ..

      - name: Angular Lint
        run: |
          cd ui && node --run lint && cd ..

      - name: Angular Unit Tests
        run: |
          cd ui && npx ng test --watch=false --browsers=ChromeHeadlessNoSandbox && cd ..

      - name: Angular Build
        run: |
          cd ui && ./node_modules/@angular/cli/bin/ng.js build --configuration production --output-hashing none --base-href / && cd ..

      - name: Upload Angular Build Artifacts
        if: matrix.node-version == '22'
        uses: actions/upload-artifact@v6
        with:
          name: angular-build
          path: ${{ github.workspace }}/ui/build/browser

  create-artifact:
    needs: [mmpm-cli, mmpm-ui]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    strategy:
      matrix:
        python-version:
          - "3.10"
          - "3.11"
          - "3.12"
          - "3.13"
          - "3.14"
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Install uv
        uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
        with:
          version: 0.8.19
          python-version: ${{ matrix.python-version }}
          enable-cache: true

      - name: Download Angular Build Artifacts
        uses: actions/download-artifact@v7
        with:
          name: angular-build
          path: ${{ github.workspace }}/ui/build/browser

      - name: Package MMPM
        run: |
          uv sync --locked --no-dev
          mkdir -p ${{ github.workspace }}/mmpm/ui

          echo "---- Contents: ui ----"
          ls -lah ${{ github.workspace }}/ui/build/browser/* # just a sanity check since angular changed the structure
          cp -r ${{ github.workspace }}/ui/build/browser/* ${{ github.workspace }}/mmpm/ui

          # sanity checks since angular changed the output structure
          echo "---- Contents: mmpm/ui ----"
          ls -lah ${{ github.workspace }}/mmpm/ui

          echo "---- Contents: mmpm/ui/media ----"
          ls -lah ${{ github.workspace }}/mmpm/ui/media

          echo "---- Contents: mmpm/ui/assets ----"
          ls -lah ${{ github.workspace }}/mmpm/ui/assets

          uv build

      - name: Upload Wheel
        if: github.event_name == 'push' && github.ref == 'refs/heads/develop' && matrix.python-version == '3.13'
        uses: actions/upload-artifact@v6
        with:
          name: mmpm-wheel
          path: ${{ github.workspace }}/dist/*.whl

      - name: Publish to PyPi
        if: startsWith(github.ref, 'refs/tags/') && matrix.python-version == '3.13'
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          user: __token__
          password: ${{ secrets.PYPI_TOKEN }}
          skip_existing: true
          verbose: true

      - name: Publish Release
        if: startsWith(github.ref, 'refs/tags/') && matrix.python-version == '3.13'
        uses: softprops/action-gh-release@v2
        with:
          files: |
            ${{ github.workspace }}/dist/*.whl
            ${{ github.workspace }}/dist/*.tar.gz

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.

name: MMPM Build Pipeline
on:
  push:
    branches:
      - "*"
    tags:
      - "*"
  pull_request:
    branches:
      - "*"
  release:
    types: [created]
 
env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
 
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
 
jobs:
  mmpm-cli:
    timeout-minutes: 30
    runs-on: latchkey-small
    strategy:
      matrix:
        python-version:
          - "3.10"
          - "3.11"
          - "3.12"
          - "3.13"
          - "3.14"
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
 
      - name: Install uv
        uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
        with:
          version: 0.8.19
          python-version: ${{ matrix.python-version }}
          enable-cache: true
 
      - name: Python Dependencies
        run: |
          uv sync --locked --all-groups
 
      - name: Python Tests
        run: |
          uv run mypy mmpm
          uv run ruff check mmpm tests
          uv run coverage run -m pytest
          uv run coverage report
 
      - name: Python Build
        run: |
          uv sync --locked --all-groups
          uv run ruff check mmpm tests
          uv build
 
      - name: Install Test
        run: |
          options=("version" "--help" "completion --help" "db --help" "guided-setup --help" "install --help" "list --help" "logs --help" "mm-ctl --help" "mm-pkg --help" "open --help" "remove --help" "search --help" "show --help" "ui --help" "update --help" "upgrade --help" "version --help")
 
          for option in "${options[@]}"; do ${{ github.workspace }}/.venv/bin/mmpm $option; done
 
      - name: Install Nix
        uses: nixbuild/nix-quick-install-action@v30
        with:
          nix_conf: |
            keep-env-derivations = true
            keep-outputs = true
 
      - name: Restore and save Nix store
        uses: nix-community/cache-nix-action@v7
        with:
          primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.lock') }} # restore and save a cache using this key
          restore-prefixes-first-match: nix-${{ runner.os }}- # if there's no cache hit, restore a cache by this prefix
          # collect garbage until the Nix store size (in bytes) is at most this number
          # before trying to save a new cache
          # 1G = 1073741824
          gc-max-store-size-linux: 1G
          purge: true # do purge caches
          purge-prefixes: nix-${{ runner.os }}- # purge all versions of the cache
          purge-created: 0 # created more than this number of seconds ago
          # or last accessed this duration (ISO 8601 duration format)
          # before the start of the `Post Restore and save Nix store` phase
          purge-last-accessed: P1DT12H
          purge-primary-key: never # except any version with the key that is the same as the `primary-key`
 
      - name: Nix Build
        run: nix build
 
      - name: "Test MMPM download"
        run: |
          python -m pip install mmpm
          mmpm_exc="${{ github.workspace }}/.venv/bin/mmpm"
 
          mmpm list --all # just to see the stdout...doing it twice is a bit wasteful, but it's fine
          mmpm list --all > database.dump
 
          exit_code=0
 
          if grep -q "breaking change on the MagicMirror 3rd Party Modules wiki" database.dump; then
              exit_code=1
          fi
 
          rm database.dump
          exit $exit_code
 
  mmpm-ui:
    timeout-minutes: 30
    runs-on: latchkey-small
    strategy:
      matrix:
        node-version: ["22", "24"]
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
 
      - name: Cache Node Modules
        uses: actions/cache@v5
        with:
          path: |
            './ui/node_modules'
          key: angular-deps-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            angular-deps-${{ hashFiles('**/package-lock.json') }}
 
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v6
        with:
          cache: 'npm'
          node-version: ${{ matrix.node-version }}
 
      - name: Angular Dependencies
        run: |
          cd ui && npm install --legacy-peer-deps && cd ..
 
      - name: Angular Lint
        run: |
          cd ui && node --run lint && cd ..
 
      - name: Angular Unit Tests
        run: |
          cd ui && npx ng test --watch=false --browsers=ChromeHeadlessNoSandbox && cd ..
 
      - name: Angular Build
        run: |
          cd ui && ./node_modules/@angular/cli/bin/ng.js build --configuration production --output-hashing none --base-href / && cd ..
 
      - name: Upload Angular Build Artifacts
        if: matrix.node-version == '22'
        uses: actions/upload-artifact@v6
        with:
          name: angular-build
          path: ${{ github.workspace }}/ui/build/browser
 
  create-artifact:
    timeout-minutes: 30
    needs: [mmpm-cli, mmpm-ui]
    runs-on: latchkey-small
    permissions:
      contents: write
    strategy:
      matrix:
        python-version:
          - "3.10"
          - "3.11"
          - "3.12"
          - "3.13"
          - "3.14"
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
 
      - name: Install uv
        uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
        with:
          version: 0.8.19
          python-version: ${{ matrix.python-version }}
          enable-cache: true
 
      - name: Download Angular Build Artifacts
        uses: actions/download-artifact@v7
        with:
          name: angular-build
          path: ${{ github.workspace }}/ui/build/browser
 
      - name: Package MMPM
        run: |
          uv sync --locked --no-dev
          mkdir -p ${{ github.workspace }}/mmpm/ui
 
          echo "---- Contents: ui ----"
          ls -lah ${{ github.workspace }}/ui/build/browser/* # just a sanity check since angular changed the structure
          cp -r ${{ github.workspace }}/ui/build/browser/* ${{ github.workspace }}/mmpm/ui
 
          # sanity checks since angular changed the output structure
          echo "---- Contents: mmpm/ui ----"
          ls -lah ${{ github.workspace }}/mmpm/ui
 
          echo "---- Contents: mmpm/ui/media ----"
          ls -lah ${{ github.workspace }}/mmpm/ui/media
 
          echo "---- Contents: mmpm/ui/assets ----"
          ls -lah ${{ github.workspace }}/mmpm/ui/assets
 
          uv build
 
      - name: Upload Wheel
        if: github.event_name == 'push' && github.ref == 'refs/heads/develop' && matrix.python-version == '3.13'
        uses: actions/upload-artifact@v6
        with:
          name: mmpm-wheel
          path: ${{ github.workspace }}/dist/*.whl
 
      - name: Publish to PyPi
        if: startsWith(github.ref, 'refs/tags/') && matrix.python-version == '3.13'
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          user: __token__
          password: ${{ secrets.PYPI_TOKEN }}
          skip_existing: true
          verbose: true
 
      - name: Publish Release
        if: startsWith(github.ref, 'refs/tags/') && matrix.python-version == '3.13'
        uses: softprops/action-gh-release@v2
        with:
          files: |
            ${{ github.workspace }}/dist/*.whl
            ${{ github.workspace }}/dist/*.tar.gz
 

What changed

4 third-party actions are referenced by a movable tag. Pin them to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.

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 3 jobs (12 with the matrix expanded) per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow