Reusable GitHub Actions for publishing software with Zero Install, wrapping the 0template, 0publish and 0repo tools.
All actions run on Linux, macOS and Windows runners and download the tools they need on-demand.
| Action | Purpose |
|---|---|
setup |
Make the 0install command available to subsequent steps |
0template |
Generate a feed (and archive) for a specific version from a template |
0publish |
Edit a feed in place, e.g. add a new version to it or sign it |
0repo-setup |
Check out a 0repo repository into the directory structure 0repo expects |
0repo |
Merge incoming feeds into the repository, sign them and regenerate the catalog |
0repo-submit |
Trigger the workflow of a 0repo repository to pick up a new release |
Makes the 0install command available to subsequent steps, downloading it if it is not already on the PATH. The other actions do this for themselves, so you only need this if you want to run 0install commands yourself.
- uses: 0install/github-actions/setup@v1
- run: 0install run https://apps.0install.net/0install/0test.xml myapp.xmlRuns 0template on a *.xml.template file, producing a feed for a single version. If the template uses local-path with a relative href, the archive is generated too.
- uses: 0install/github-actions/0template@v1
id: feed
with:
template: myapp.xml.template
version: '1.2.3'| Input | Description |
|---|---|
template |
The path of the *.xml.template file. Required. |
version |
The version number of the release. Required. Write in quotes to avoid accidental interpretation as a floating point number (e.g., '1.0' instead of 1.0). |
args |
Additional whitespace-separated key=value parameters for the template, e.g. released=2026-08-02. |
archive-url |
The URL the generated archive will be published at. Relative hrefs in the generated feed are replaced with this URL. A value ending in / is treated as a directory and the archive's file name is appended. |
github-release |
Set to true if the generated archive will be attached to the GitHub Release for the current tag. Shorthand for archive-url: https://github.com/OWNER/REPO/releases/download/TAG/. |
upload-artifact |
Set to false to keep the generated feed and archives from being uploaded as a GitHub Actions artifact. Defaults to true. |
artifact-name |
The name of that artifact. Defaults to feed. Artifact names must be unique within a workflow run, so override this if you run the action more than once (e.g. in a matrix job). |
| Output | Description |
|---|---|
feed |
The path of the generated feed, e.g. myapp-1.2.3.xml. |
archive |
The paths of the generated archives, one per line. Empty if the feed references external archives. |
0template writes its output next to the template and resolves local-path relative to it, so a template in a subdirectory produces its feed in that subdirectory. The outputs are relative to the workspace either way, so they can be passed straight to other actions:
- uses: softprops/action-gh-release@v3
with:
files: |
${{steps.feed.outputs.feed}}
${{steps.feed.outputs.archive}}The files are also uploaded as a GitHub Actions artifact, so they can be inspected or downloaded by a later job even on builds that do not publish anything.
Feeds usually need to point at an absolute URL, while 0template needs a relative href to generate the archive. Use archive-url to bridge the two, or github-release if the archive is going to be an asset of the GitHub Release for the current tag:
- uses: 0install/github-actions/0template@v1
with:
template: myapp.xml.template
version: ${{github.ref_name}}
github-release: trueis equivalent to:
archive-url: https://github.com/${{github.repository}}/releases/download/${{github.ref_name}}/Runs 0publish on a feed, editing it in place. Typically used to merge a newly generated per-version feed into a master feed that lists all versions.
- uses: 0install/github-actions/0publish@v1
with:
feed: gh-pages/myapp.xml
add-from: ${{steps.feed.outputs.feed}}
gpg-key: ${{secrets.GPG_KEY}}| Input | Description |
|---|---|
feed |
The path of the feed to edit. Required. |
add-from |
The path or URL of a feed whose implementations should be added to the feed. |
set-released |
The release date to set in the format YYYY-MM-DD (or today). |
set-stability |
The stability rating to set (stable, testing, developer, ...). |
key |
The ID of the GnuPG key to sign with. Defaults to the key the feed was previously signed with. |
xmlsign |
Set to true to add a signature to a previously unsigned feed and export the public key next to it. |
gpg-key |
An ASCII-armored GnuPG secret key to import before running 0publish. |
args |
Additional whitespace-separated arguments to pass to 0publish. |
If the feed was already signed, 0publish resigns it with the same key, so gpg-key needs to hold that key. Export it with gpg --export-secret-keys --armor YOURKEY and store it as a repository secret. The key must not be protected by a passphrase.
Checks out a 0repo repository into the directory structure 0repo expects.
- uses: 0install/github-actions/0repo-setup@v1This replaces the actions/checkout step that usually sits at the top of a workflow. 0repo works on two branches of the same repository at once, and needs them next to each other rather than at the workspace root, so this action performs both checkouts itself and creates the scaffolding around them:
feeds/ # default branch: 0repo-config.py, the unsigned feeds and templates
public/ # gh-pages branch: the published (signed) feeds and archives.db
incoming/ # empty, holds feeds waiting to be merged
0repo-config.py # symlink to feeds/0repo-config.py
archives.db # symlink to public/archives.db
Later steps in the job therefore work inside these directories rather than at the workspace root.
| Input | Description |
|---|---|
repository |
The repository holding the 0repo configuration and the unsigned feeds. Defaults to the current repository. |
ref |
The branch, tag or SHA to check out the feeds from. Defaults to the default branch. |
public-ref |
The branch holding the published (signed) feeds. Defaults to gh-pages. |
token |
The token to use for checking out and (later) pushing the repository. Defaults to github.token. |
git-user-name, git-user-email |
The identity to use for Git commits. |
Runs 0repo, which validates the feeds in incoming/, merges them into the master feeds, signs everything and regenerates the catalog.
- uses: 0install/github-actions/0repo@v1
with:
gpg-key: ${{secrets.GPG_KEY}}| Input | Description |
|---|---|
incoming-feed-url |
The URL of a feed to download into incoming/ before running 0repo. |
incoming-archive-url |
The URL of an archive to register in archives.db. |
gpg-key |
An ASCII-armored GnuPG secret key to sign the feeds with. |
push-feeds |
Set to true to push the merged feeds in feeds/ back to the repository. |
push-public |
Set to true to push the published feeds in public/ back to the repository. |
working-directory |
The directory containing the 0repo directory structure. Defaults to the workspace root. |
args |
Additional whitespace-separated arguments to pass to 0repo. |
0repo commits its results to both checkouts made by 0repo-setup: the published feeds to public/ and the merged feeds to feeds/.
By default it leaves those commits sitting in the workspace, which is what you want when validating a pull request, or when the changes are going into a pull request of their own. You can configure the action to push them for you, so the workflow needs no git push step of its own:
- uses: 0install/github-actions/0repo@v1
with:
gpg-key: ${{secrets.GPG_KEY}}
push-feeds: true
push-public: trueIf gpg-key is not set, 0repo runs with the environment variable NO_SIGN set. Have your 0repo-config.py honor it, so that pull requests can be validated without exposing the production key:
GPG_SIGNING_KEY = None if os.getenv('NO_SIGN') else "0xYOURKEYFINGERPRINT"Such a run produces unsigned feeds, which must never end up on the published branch. public/ is therefore only pushed when gpg-key is set, even if push-public is true.
incoming-feed-url fetches a feed published elsewhere into incoming/, so a single step covers the whole "a new release arrived" case:
- uses: 0install/github-actions/0repo@v1
with:
incoming-feed-url: ${{inputs.feed_url}}
incoming-archive-url: ${{inputs.archive_url}}
gpg-key: ${{secrets.GPG_KEY}}
push-feeds: true
push-public: trueArchives hosted by the repository itself are handled by 0repo. incoming-archive-url is only needed for archives hosted elsewhere (typically as assets of a GitHub Release in the application's own repository): the archive is downloaded, hashed and registered in archives.db so that 0repo can point the published feed at it.
Triggers a workflow_dispatch workflow in a 0repo repository, telling it to pick up a newly released feed. Use this in the workflow of an application that publishes into a central feed repository.
- uses: 0install/github-actions/0repo-submit@v1
with:
repository: 0install/apps
feed-url: ${{steps.feed.outputs.feed}}
archive-url: ${{steps.feed.outputs.archive}}
token: ${{secrets.PERSONAL_TOKEN}}| Input | Description |
|---|---|
repository |
The repository running 0repo, e.g. 0install/apps. Required. |
feed-url |
The URL of the feed to merge into the repository. Required. |
archive-url |
The URL of an archive to register in archives.db. |
token |
A token with Actions: write permission for the target repository. Required. |
workflow |
The name of the workflow to trigger. Defaults to Incoming. |
ref |
The branch or tag to run the workflow from. |
feed-url and archive-url may be relative instead of absolute, in which case they are resolved against the GitHub Release for the current tag. The example above is therefore equivalent to:
feed-url: https://github.com/${{github.repository}}/releases/download/${{github.ref_name}}/myapp-1.2.3.xml
archive-url: https://github.com/${{github.repository}}/releases/download/${{github.ref_name}}/myapp-1.2.3.tar.gzSince assets of a GitHub Release are flat, only the file name of a relative path is used. That way the paths output by the 0template action can be passed straight through, even for a template in a subdirectory.
The default GITHUB_TOKEN only has permissions for the current repository, so token needs to be a (fine-grained) personal access token stored as a secret.
The master feed lives on the gh-pages branch of the same repository. Each release generates a feed for the new version, merges it into the master feed and pushes it back.
name: Release
on:
push:
tags: ['*']
jobs:
release:
runs-on: ubuntu-latest
permissions: {contents: write}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7
with: {ref: gh-pages, path: gh-pages}
- name: Build
run: ./build.sh ${{github.ref_name}}
- name: Generate feed
id: feed
uses: 0install/github-actions/0template@v1
with:
template: myapp.xml.template
version: ${{github.ref_name}}
github-release: true
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
files: |
${{steps.feed.outputs.feed}}
${{steps.feed.outputs.archive}}
- name: Add to master feed
uses: 0install/github-actions/0publish@v1
with:
feed: gh-pages/myapp.xml
add-from: ${{steps.feed.outputs.feed}}
gpg-key: ${{secrets.GPG_KEY}}
- name: Push gh-pages
working-directory: gh-pages
run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
git commit -am "Published ${{github.ref_name}}"
git pushThe application's workflow attaches the feed and the archive to a GitHub Release and hands both URLs to the central feed repository:
- name: Generate feed
id: feed
uses: 0install/github-actions/0template@v1
with:
template: myapp.xml.template
version: ${{github.ref_name}}
github-release: true
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
files: |
${{steps.feed.outputs.feed}}
${{steps.feed.outputs.archive}}
- name: Submit feed
uses: 0install/github-actions/0repo-submit@v1
with:
repository: myname/feeds
feed-url: ${{steps.feed.outputs.feed}}
archive-url: ${{steps.feed.outputs.archive}}
token: ${{secrets.PERSONAL_TOKEN}}The matching Incoming workflow in the feed repository:
name: Incoming
on:
workflow_dispatch:
inputs:
feed_url: {required: true, description: URL of feed to merge in}
archive_url: {required: false, description: URL of archive to add to archives.db}
concurrency: {group: publish}
jobs:
incoming:
runs-on: ubuntu-latest
steps:
# Checks out the repository; no actions/checkout needed
- uses: 0install/github-actions/0repo-setup@v1
- uses: 0install/github-actions/0repo@v1
with:
incoming-feed-url: ${{inputs.feed_url}}
incoming-archive-url: ${{inputs.archive_url}}
gpg-key: ${{secrets.GPG_KEY}}
push-feeds: true
push-public: trueSee the packaging tutorials for the bigger picture and 0install/apps for a real-world deployment.