Quartet / Quintet

2026-09-04  ·  githubactions · cli · ci · typescript

Distribute Custom Tools as Composite GitHub Actions (Without npm Publishing)

When you create a small CLI tool, it is common to consider publishing it on npm for others to use. However, there is a distribution path that requires no account or publishing process: GitHub Actions.

- uses: your-name/tool-name@v1

With this single line, the tool becomes usable. I have done this myself, so here are the steps and common pitfalls.

Composite vs Docker

There are three types of Actions (JavaScript / Docker / composite), but if the tool runs on Node, composite is the only choice.

composite Docker
Startup Fast Requires building or pulling an image
Execution Environment Uses the runner's environment Isolated
Implementation Just list shell steps Requires a Dockerfile

Docker isolation is necessary for tools that modify the system or require bundling special dependencies. Using Docker for a tool that completes with just Node yields no return on the startup cost.

Full Code

Place action.yml in the root of your repository.

name: 'mdlinkcheck'
description: 'Find broken relative links in Markdown files.'
author: 'your-name'
branding:
  icon: 'link-2'
  color: 'orange'

inputs:
  path:
    description: 'Directory or file to check.'
    required: false
    default: '.'
  format:
    description: 'Output format: text or json.'
    required: false
    default: 'text'

runs:
  using: 'composite'
  steps:
    - name: Set up Node
      uses: actions/setup-node@v4
      with:
        node-version: '22'

    - name: Build
      shell: bash
      working-directory: ${{ github.action_path }}
      run: |
        npm ci --no-audit --no-fund
        npm run build

    - name: Check links
      shell: bash
      run: |
        node "${{ github.action_path }}/bin/mdlinkcheck.js" \
          "${{ inputs.path }}" --format "${{ inputs.format }}"

That is all.

Pitfalls

shell: is required for every step

Composite run steps will error if you do not specify shell: for each one. It can be omitted in normal workflows, so it is easy to forget.

Use ${{ github.action_path }}

This is the absolute path of the directory where the action is located. Since it runs in the user's repository, the current directory belongs to the user.

# NG: Tries to run npm ci in the user's repository
run: npm ci

# OK
working-directory: ${{ github.action_path }}
run: npm ci

Specify the tool execution with an absolute path as well.

run: node "${{ github.action_path }}/bin/tool.js" "${{ inputs.path }}"

Exit codes are passed through directly

If a shell step ends with a non-zero exit code, the step fails. Unless you add || true or write continue-on-error, the tool's exit code becomes the CI pass/fail result.

This is effective, so it is worth designing your tool's exit codes properly.

0  No issues
1  Issues found (CI should fail)
2  Usage error (missing path, invalid option)

Whether to commit dist/

With JavaScript actions, you must commit dist/, but with composite, you can just run npm ci && npm run build on the action side, so it is unnecessary.

This means a build runs every time. It takes a few seconds, which I accept. If this concerns you, you can use actions/cache.

Tagging Versions

Guiding users to @main means users will inadvertently pull breaking changes.

git tag -a v1.0.0 -m "v1.0.0"
git push origin v1.0.0

# v1 is the "working tag". It always points to the latest 1.x
git tag -f v1
git push -f origin v1

Guide users to use @v1, and let them know that @v1.0.0 is available for pinning. Advance v1 with each release.

git tag -f v1 && git push -f origin v1

Verify Published Tags Work in CI

This was the most effective step.

It is easy to verify functionality by using uses: ./ in your own repository, but even if that passes, if the commit pointed to by the tag is broken, users will not be able to use it. This is because ./ only looks at the local checkout.

Therefore, add a job to CI that "executes by referencing the published tags."

jobs:
  use-the-action:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./                      # Local
        with:
          path: .

  published-tag:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: your-name/your-tool@v1  # Published tag
        with:
          path: .

With this, your CI will fail the moment you make a mistake in retagging. You can catch it before it fails in users' repositories.

For tools that can run against themselves (linters, formatters, link checkers, etc.), this format is particularly suitable. "Usable" becomes more than just a claim; it is verified every time.

Whether to Publish to the Marketplace

If you write branding in action.yml, GitHub's UI will prompt you to publish to the Marketplace. You do not need to rush, as it can be used via uses: even without publishing.

Publishing adds one more discovery path, so it is worth doing after refining your repository description and topics.

Summary

I have open-sourced a small CLI (Markdown link checker) distributed in this format under the MIT license.


I publish the configuration for splitting Claude Code into separate personas — Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run ./setup.sh, and it works. It does not depend on your tech stack.

https://github.com/quintetkit/quartet

I built one real tool using nothing but this workflow. Every Issue, PR, review and merge is still there. The parts that went wrong were not deleted.

https://github.com/quintetkit/mdlinkcheck

The version that adds a UI Designer persona, review criteria, a per-Issue parallel execution script and a 10-chapter guide is on the product page.

The workflow itself is available

Quartet, the four-persona version, is published free under MIT. Quintet adds a UI Designer persona, review criteria, a per-Issue parallel execution script, and a 10-chapter guide.

See the free version Product page