Extensions on GitHub

Author

Brady Johnston

Published

September 19, 2025

These slides and accompanying blog post were created for my talk at the 2025 Blender conference.

See the slides below of click directly on this link.

View slides in full screen

Who am I?

I’m Brady!

Molecular Nodes

This hasn’t been the first attempt at bringing molecular data sets into Blender, but I blieve it has so far been the most successful. In a very short summary, we can think of atoms and molecules as a collection of vertices and edges. Blender knows how to handle vertices and edges. Once we have the data inside of Blender (along with a large number of ‘molecular’ attributes for points and edges) then the full power of Geometry Nodes is at our fingertips for molecular visualisation.

Figure 1: A clathrin protein cage appearing, or summoning a protein via a DNA circle.

Install as a python package?

It’s less likely than you’d think!

Creating an add-on for Blender that can also be built and shipping as a python package was quite the task. It leads to a variety of problems and strange engineering issues that come with writing code for Blender that a ‘regular’ python package wouldn’t have to deal with.

Covering that topic with all of our learnings would be a massive blog post itself. The short version is that you can install molecularnodes from pypi and use if for molecular rendering in python scripts of jupyter notebooks like this one.

# !pip install bpy molecularnodes

import molecularnodes as mn
canvas = mn.Canvas(
    engine=mn.scene.Cycles(samples=16),
    resolution=(1280, 720),
    transparent=True
    )

mol = mn.Molecule.fetch("1BNA")
mol.add_style("vdw")

canvas.frame_view(mol, (3.14, 3.14 / 2, 0))
canvas.snapshot()
Figure 2: X-ray crystallography structure of a fragment of DNA.

databpy

  • ‘numpy-like’ interfaces to attributes on objects
# set the x component for the first 10 verts to 1.0
obj["position"][:10, 0] = 1.0 

# add 1 to x, 2 to y, 3 to z component of position
obj["position"] += np.array([1, 2, 3]) 

# infers data types for storage and retrieval
obj["att_name"] = np.zeros(len(obj), dypte=int)
obj["att_name"] 

# get and set attributes on non-point domains
obj.store_named_attribute(
  data=np.zeros((len(bob), 4, 4), dtype=float),
  name="a_matrix",
  domain=db.AttributeDomains.FACE
  )
obj.named_attribute()

Building on GitHub

Why build on GitHub?

Building & Distributing MN

Building & Distributing MN

  • Run tests against different versions of Blender (including daily builds)
  • Run tests against bpy installed via pip for automated pipelines
  • Download and bundle .whl files with extension
  • Upload extension to GitHub & Extensions platform

Testing your Add-ons

Checko out Spencer’s talk on testing add-ons here.

Download PDF File

Unable to display PDF file. Download instead.

Anatomy of a GitHub Action

  • Runs on hardware managed by GitHub
    • Free for public projects
    • Mac / Windows / Linux
    • Mid-power machines, no GPU
  • Can use your own hardware as a GHA runner
    • My PC with an RTX 3090 runs any GPU worklods back in Australia

Running Tests

name: tests

on: 
    push:
      branches: ["main"]
    pull_request:
      branches: ["*"]
    
jobs:
    blender:
        runs-on: ${{ matrix.os }}
        strategy:
            max-parallel: 4
            fail-fast: false
            matrix:
              version: ["4.5", "daily"]
              os: [ubuntu-latest, macos-latest, windows-latest]
        steps:
            - uses: actions/checkout@v4
            - uses: BradyAJohnston/setup-blender@v4
              with:
                version: ${{ matrix.version }}
            - name: Install in Blender
              run: |
                blender -b -P tests/python.py -- -m pip install ".[test]"
            - name: Run Tests
              run: |
                blender -b -P tests/run.py -- -vv tests --cov --cov-report=xml
    
            - name: Expose coverage as a CI download 
              uses: actions/upload-artifact@v4
              if: matrix.os == 'ubuntu-latest' && matrix.version == '4.2.5'
              with:
                name: coverage.xml
                path: coverage.xml

            - name: Upload coverage reports to Codecov
              if: matrix.os == 'ubuntu-latest'
              uses: codecov/codecov-action@v3

Breaking 5.0 Changes

Tests are run daily on MacOS, Windows & Linux against the latest alpha builds from Blender.

  • BLENDER_EEVEE_NEXT renamed to BLENDER_EEVEE #855
  • Compositor no longer has use_nodes #971
  • Set Curve Resolution not updating datablock #141721
  • bpy.data.materials.new() starts with empty node tree (not BSDF + Output) #980

These were some of the recent changes that were breaking the add-on but automated testing alterted us and we were able to fix it quickly. When 5.0 is released as long as we keep fixing the problems as they appear we can be confident that it will work day-of.

Fixing individual errors as they appear is also much easier to debug than testing once the beta releases happen (or worse the final release) where multiple failures might pile up on top of each other.

Distributing on GitHub

‘Old School’

  • have your add-on be ‘top-level’ on GitHub - users just download the .zip of the repo and install
  • Never clear when updates are out or what version is ‘released’
  • package up .zip and create a GitHub release - users download from release page and check back for updates
  • Clearer which version is currently released, requires users to manually update

Hip & New and Cool and Fresh with ‘Extensions’

  • Host an index.json somewhere accessible on the network which points to files, allows automatic updates
  • Host an index.json and files all on GitHub
    • Simple index for single add-on on a single repo
    • A repo that indexes and builds multiple add-ons from multiple sources

A live example

Figure 3: The hottest new monkey-spawning add-on! (But is there a bug?)

This is a minimal example of an add-on we can subscribe to.

Your Personal Extensions Repo

  • Build and release add-ons on GitHub (through automations!) in their own repos
  • Compile multiple add-ons in a single subscribeable repo
  • Users add ‘your personal repo’ - they get updates just like they would from the official Extensions platform

How is it Built?

We can use GitHub actions (or another similar service) to compile ‘released’ add-ons from multiple different services into a single extenion repo.

Importantly we need Blender to run checksum on the actual files that your users will download before publishing the index.json. We don’t host or serve the actual files ourselves, those remain on at the original publishing locations (the release pages of the individual repos). We just generate an index that points to those locations.

name: Download Release Files

on:
  push:
    branches: ["main"]
  schedule:
    - cron: '0 0 * * *'

jobs:
  build-website:
    runs-on: macos-latest
    permissions: write-all
    steps:
      - uses: actions/checkout@v4
      - uses: quarto-dev/quarto-actions/setup@v2
      - uses: bradyajohnston/setup-blender@v4
        with: 
          version: latest
      - name: Install uv
        uses: astral-sh/setup-uv@v4
        with:
            version: "latest"
    
      - name: Get Release Assets
        run: |
          uv run --with requests scripts/download.py
        env:
          GITHUB_TOKEN: ${{ github.token }}
      - name: Generate index.json
        run: |
          blender --command extension server-generate --repo-dir=repo
    
      - name: Quarto Render
        run: |
          uv run --with pyyaml quarto render
    
      - name: Publish to GitHub Pages (and render)
        uses: quarto-dev/quarto-actions/publish@v2
        with:
            target: gh-pages
            path: .
            render: false
        env:
            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Distributing your own Extensions

Details and built: https://bradyajohnston.github.io/extensions/ Subscribe with:

https://bradyajohnston.github.io/extensions/index.json

Blext

Thanks!