Skip to content

Repository files navigation

Connect Gallery Action

A collection of GitHub Actions for managing a Posit Connect extension gallery. These actions handle linting, releasing, and generating a gallery index (extensions.json) from extension manifests and GitHub Releases.

Quickstart Examples

Simple extensions

For extensions that can be packaged directly by creating a tarball of their directory, you just need a directory containing all of the extensions (e.g. ./extensions/), and a gallery.json file at the root of your repo that list the categories your extensions fit in (see below for the format of that file).

name: Publish Extension Workflow

on:
  pull_request:
  push:
    branches:
      - main

permissions:
  contents: write

jobs:
  # Lint, package, and release each extension
  extensions:
    strategy:
      fail-fast: false
      matrix:
        extension:
          - my-extension
          - another-extension
    runs-on: ubuntu-latest
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - uses: actions/checkout@v4

      - uses: posit-dev/connect-gallery-action/build-extension@main
        with:
          extension-name: ${{ matrix.extension }}

  # Regenerate extensions.json and commit
  update-gallery:
    runs-on: ubuntu-latest
    needs: [extensions]
    if: ${{ always() }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: main

      - uses: posit-dev/connect-gallery-action@main
        with:
          extensions-dir: extensions

And then, to use this in a Connect instance, you'll add the following to the config file. For both the URL and Repo, change those values to point to your GitHub org and repo.

[GalleryFeed "My feed"]
URL = "https://raw.githubusercontent.com/{org}/{repo}/refs/heads/main/extensions.json"
Name = "My Extensions"
Description = "Internal extensions"
Repo = "https://github.com/{org}/{repo}"

Actions

This repository provides five composite actions. build-extension is a bundle of
lint-extension, package-extension and release-extension. They are exposed separately if you need more complex setups (see the advanced section below)

Action Description
posit-dev/connect-gallery-action@main Generates extensions.json from extension manifests and GitHub Releases
posit-dev/connect-gallery-action/build-extension@main Lints, packages, and optionally releases an extension in a single step
posit-dev/connect-gallery-action/lint-extension@main Validates an extension manifest for required fields and structure
posit-dev/connect-gallery-action/package-extension@main Packages an extension into a tarball and uploads it as a workflow artifact
posit-dev/connect-gallery-action/release-extension@main Creates a GitHub Release for a single extension when its version is bumped

Structure

To use these actions you need a repository with the following structure:

your-repo/
  extensions/
    my-extension/
      manifest.json
      ...
    another-extension/
      manifest.json
      ...
  gallery.json

gallery.json

Defines the categories available in the gallery:

{
  "categories": [
    {
      "id": "extension",
      "title": "extensions",
      "description": "Useful tools that extend the functionality of Connect."
    },
    {
      "id": "example",
      "title": "examples",
      "description": "Pre-built content to illustrate publishable content types."
    }
  ]
}

Extension manifest.json

Each extension directory must contain a manifest.json with an extension section. This is in addition to the standard manifest fields generated by rsconnect or rsconnect-python.

{
  "extension": {
    "name": "my-extension",
    "title": "My Extension",
    "description": "A description of the extension.",
    "homepage": "https://github.com/your-org/your-repo/tree/main/extensions/my-extension",
    "category": "extension",
    "tags": ["python"],
    "minimumConnectVersion": "2025.04.0",
    "requiredFeatures": [],
    "version": "0.0.0"
  },
  "environment": {
    "python": {
      "requires": "~=3.8"
    }
  }
}

Required fields:

  • name -- Must match the extension's directory name
  • title -- Display name in the gallery
  • description -- Description shown in the gallery
  • homepage -- Link to documentation or source
  • minimumConnectVersion -- Minimum Posit Connect version required
  • version -- Semantic version (see Versioning)

Optional fields:

  • category -- Must match a category id from gallery.json
  • tags -- Array of tags for filtering in the gallery
  • requiredFeatures -- Array of Connect features required (e.g., ["API Publishing"])

Environment section:

The environment section specifies language version constraints using the ~= (compatible release) operator:

{
  "environment": {
    "python": { "requires": "~=3.8" },
    "r": { "requires": "~=4.2" },
    "quarto": { "requires": "~=1.3" }
  }
}

Versioning

Extensions use semantic versioning. The version in manifest.json controls releases:

  • 0.0.0 -- Reserved. Will never be released. Use this during development.
  • Any other valid semver -- A GitHub Release is created when the version in manifest.json is greater than the latest released version and the change is merged to main.

Detailed Usage

Generate Gallery

Scans extension manifests and GitHub Releases to generate extensions.json.

- uses: posit-dev/connect-gallery-action@main
  with:
    extensions-dir: extensions
    gallery-config: gallery.json

Inputs

Input Required Default Description
extensions-dir No extensions Relative path to the extensions directory
gallery-config No gallery.json Relative path to the gallery config file
commit No true Whether to commit and push extensions.json when updates are generated

Outputs

Output Description
has-updates true if extensions.json was generated (at least one released extension exists)

Build Extension

Lints, packages, and optionally releases an extension in a single step. This is a convenience wrapper around lint-extension, package-extension, and release-extension.

- uses: posit-dev/connect-gallery-action/build-extension@main
  with:
    extension-name: my-extension

To skip the release step (e.g., on pull requests where you only want to lint and package):

- uses: posit-dev/connect-gallery-action/build-extension@main
  with:
    extension-name: my-extension
    release: "false"

Inputs

Input Required Default Description
extension-name Yes The name of the extension (must match its directory name)
artifact-name No Name of an uploaded artifact to use as the tarball source. If omitted, the extension directory is tarred directly.
release No "true" Whether to run the release step
extensions-dir No "extensions" Relative path from workspace root to extensions directory

Lint Extension

Validates that an extension manifest has all required fields, proper structure, and a valid semver version.

- uses: posit-dev/connect-gallery-action/lint-extension@main
  with:
    extension-name: my-extension

Inputs

Input Required Default Description
extension-name Yes The name of the extension (must match its directory name)
extensions-dir No "extensions" Relative path from workspace root to extensions directory

Checks performed

  1. Extension name matches directory name and name in manifest.json
  2. All required fields are present and correctly typed
  3. requiredFeatures and tags are arrays (if present)
  4. Version is valid semver

Package Extension

Packages an extension into a {extension-name}.tar.gz tarball and uploads it as a workflow artifact for use by subsequent jobs (e.g., testing or release).

For simple extensions (where the directory can be tarred as-is):

- uses: posit-dev/connect-gallery-action/package-extension@main
  with:
    extension-name: my-extension

For custom-built extensions, first upload an artifact containing only the runtime files, then pass its name via artifact-name. The action will download that artifact and create the tarball from it instead of the source directory:

- uses: posit-dev/connect-gallery-action/package-extension@main
  with:
    extension-name: my-extension
    artifact-name: my-extension

Inputs

Input Required Default Description
extension-name Yes The name of the extension (must match its directory name)
artifact-name No Name of an uploaded artifact to use as the tarball source. If omitted, the extension directory is tarred directly.
extensions-dir No "extensions" Relative path from workspace root to extensions directory

Release Extension

Creates a GitHub Release when an extension's manifest version is bumped. The release includes the packaged extension as an asset and manifest metadata in the release body.

- uses: posit-dev/connect-gallery-action/release-extension@main
  with:
    extension-name: my-extension

Requires GH_TOKEN to be set in the environment for gh CLI access.

The release tag follows the format {extension-name}@v{version} (e.g., my-extension@v1.0.0). This action expects a {extension-name}.tar.gz artifact to have been uploaded by a previous step in the workflow.

Inputs

Input Required Default Description
extension-name Yes The name of the extension
extensions-dir No "extensions" Relative path from workspace root to extensions directory

Workflow Examples

Simple extensions

For extensions that can be packaged directly by creating a tarball of their directory, use a single workflow that handles change detection, linting, packaging, testing, releasing, and gallery generation.

name: Extension Workflow

on:
  pull_request:
  push:
    branches:
      - main

permissions:
  contents: write

jobs:
  # Detect which extensions changed
  extension-changes:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: read
    outputs:
      changes: ${{ steps.changes.outputs.changes }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: changes
        with:
          filters: |
            my-extension: extensions/my-extension/**
            another-extension: extensions/another-extension/**

  # Lint, package, and release each changed extension
  extensions:
    needs: [extension-changes]
    if: ${{ needs.extension-changes.outputs.changes != '[]' }}
    strategy:
      fail-fast: false
      matrix:
        extension: ${{ fromJSON(needs.extension-changes.outputs.changes) }}
    runs-on: ubuntu-latest
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - uses: actions/checkout@v4

      - uses: posit-dev/connect-gallery-action/build-extension@main
        with:
          extension-name: ${{ matrix.extension }}

  # Regenerate extensions.json and commit
  update-gallery:
    runs-on: ubuntu-latest
    needs: [extensions]
    if: ${{ always() }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: main

      - uses: posit-dev/connect-gallery-action@main
        with:
          extensions-dir: extensions

How It Works

  1. Lint -- lint-extension validates the manifest structure before any packaging or release steps run.

  2. Package -- package-extension packages the extension into a {name}.tar.gz tarball and uploads it as a workflow artifact.

  3. Release -- release-extension compares the manifest version against the latest GitHub Release tag ({name}@v{version}). If the manifest version is strictly greater and the workflow is running on main, it creates a new GitHub Release with the tarball as an asset and manifest metadata in the release body.

  4. Generate -- The main action scans all extension manifests, queries all GitHub Releases (up to 1000), and assembles extensions.json containing categories, tags, required features, and all extension versions sorted by semver.

Custom-built extensions (advanced mode)

Extensions that require build steps (e.g., compiling TypeScript, bundling assets) before packaging need a custom workflow. The key difference is uploading only the built artifacts, then using a packaging step that creates the tarball from those artifacts rather than the full source directory.

name: My Custom Extension

on:
  workflow_call:

env:
  EXTENSION_NAME: my-custom-extension
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
  extension:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./extensions/${{ env.EXTENSION_NAME }}
    steps:
      - uses: actions/checkout@v4

      - uses: posit-dev/connect-gallery-action/lint-extension@main
        with:
          extension-name: ${{ env.EXTENSION_NAME }}

      # Custom build steps
      - uses: actions/setup-node@v4
        with:
          node-version: "lts/*"
      - run: npm ci
      - run: npm run build

      # Upload only the files needed at runtime
      - uses: actions/upload-artifact@v4
        with:
          name: ${{ env.EXTENSION_NAME }}
          path: |
            extensions/${{ env.EXTENSION_NAME }}/dist/
            extensions/${{ env.EXTENSION_NAME }}/requirements.txt
            extensions/${{ env.EXTENSION_NAME }}/app.py
            extensions/${{ env.EXTENSION_NAME }}/manifest.json

      # Package the uploaded artifact into a tarball
      - uses: posit-dev/connect-gallery-action/package-extension@main
        with:
          extension-name: ${{ env.EXTENSION_NAME }}
          artifact-name: ${{ env.EXTENSION_NAME }}

  release:
    runs-on: ubuntu-latest
    needs: [extension]
    steps:
      - uses: actions/checkout@v4

      - uses: posit-dev/connect-gallery-action/release-extension@main
        with:
          extension-name: ${{ env.EXTENSION_NAME }}

This custom workflow is then called from the main extensions workflow:

  my-custom-extension:
    needs: [complex-extension-changes]
    if: ${{ needs.complex-extension-changes.outputs.my-custom-extension == 'true' }}
    uses: ./.github/workflows/my-custom-extension.yml
    secrets: inherit

Development

Scripts

The gallery generation logic lives in scripts/:

cd scripts
npm ci
npm test              # Run tests
npm run generate-gallery  # Build and run the generator

CI

Tests run automatically on push and pull requests to main via .github/workflows/ci.yml.

About

a GitHub action for generating Connect gallery feeds

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages