Managing multiple feeds with 0repo¶
Once you have more than one feed, the per-repo 0template + 0publish workflow from the previous tutorial starts to chafe. Each app needs its own GPG key copy, its own gh-pages branch, its own merge logic. There's no shared catalog, no shared key, no consistent enforcement of policies like "every feed must declare a license" or "release dates must be set".
0repo replaces the per-repo plumbing with a single, central feed repository. It:
- Validates each incoming feed against your repository's policies (license required, release date set, signed by an authorized key, ...).
- Merges new implementations into the right main feed.
- Signs the result with the repository's GPG key.
- Generates a top-level catalog and a directory listing.
The key shift from the previous tutorial: each app's *.xml.template keeps living in the app's own source repo (alongside the source code). The app's CI builds the release, runs 0template, attaches the per-version feed to a GitHub Release, and tells the central feed repo to pick it up. The central repo runs 0repo and publishes.
This tutorial assumes you've worked through Automating a single feed with CI, so the source-repo side will look familiar.
By the end you will have:
- A central
feedsrepository with0repo-config.pyonmainand signed feeds ongh-pages. Served athttps://YOURNAME.github.io/feeds/. - One or more app source repositories, each with its own
*.xml.templateand a CI workflow that releases through the central repo. - An
Incomingworkflow on the central repo that runs0repowhenever an app finishes a release.
1. Create the central feed repository¶
Create a new repo feeds (any name works). Initialise an empty gh-pages orphan branch and push it:
git clone https://github.com/YOURNAME/feeds.git
cd feeds
git checkout --orphan gh-pages
git rm -rf .
echo "" > archives.db
git add archives.db
git commit -m "Initial gh-pages"
git push -u origin gh-pages
git checkout main
In Settings → Pages, set the source to Deploy from a branch / gh-pages / root. Note the URL; let's say https://YOURNAME.github.io/feeds/. This is your repository base URL.
2. Initialise 0repo¶
0install add 0repo https://apps.0install.net/0install/0repo.xml
0repo create ~/repos/feeds 'Your Name'
0repo create writes a 0repo-config.py. Move it into your Git checkout and edit REPOSITORY_BASE_URL and GPG_SIGNING_KEY:
REPOSITORY_BASE_URL = "https://YOURNAME.github.io/feeds/"
GPG_SIGNING_KEY = None if os.getenv('NO_SIGN') else "0xYOURKEYFINGERPRINT"
The NO_SIGN escape hatch lets CI run 0repo in dry-run mode for pull-request validation without exposing the production key.
Commit 0repo-config.py to main and push.
3. Add the GPG key as a repository secret¶
Export the private key:
gpg --export-secret-keys --armor YOURKEY
In the feeds repo's Settings → Secrets and variables → Actions, create a secret named GPG_KEY with the armored key as its value.
4. Add publish & incoming workflows¶
Two workflows on the feeds repo do the heavy lifting:
publish.ymlruns whenever0repo-config.pychanges onmain(so policy edits take effect without a feed change).incoming.ymlis a manually triggered workflow that pulls a fresh feed from a URL, runs0repoto merge and sign it, and pushesgh-pages. App repos kick this off viagh workflow run.
.github/workflows/publish.yml:
name: Publish
on:
workflow_dispatch: {}
push:
branches: [main]
paths: ['0repo-config.py']
concurrency: { group: publish }
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { path: feeds, fetch-depth: 0 }
- uses: actions/checkout@v4
with: { path: public, ref: gh-pages }
- name: Set up directory structure
run: |
mkdir incoming
ln -s feeds/0repo-config.py .
ln -s public/archives.db .
git config --global user.name 'CI'
git config --global user.email 'ci@example.com'
- name: Import GPG key
run: echo "${{ secrets.GPG_KEY }}" | gpg --import -
- name: Run 0repo
run: |
curl -sSfLO https://get.0install.net/0install.sh && chmod +x 0install.sh
./0install.sh run https://apps.0install.net/0install/0repo.xml
- name: Push public
run: cd public && git push
.github/workflows/incoming.yml:
name: Incoming
on:
workflow_dispatch:
inputs:
feed_url:
required: true
description: URL of the per-version feed to merge in
archive_url:
required: false
description: URL of the archive to register in archives.db
concurrency: { group: publish }
jobs:
incoming:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { path: feeds, fetch-depth: 0 }
- uses: actions/checkout@v4
with: { path: public, ref: gh-pages }
- name: Set up directory structure
run: |
mkdir incoming
ln -s feeds/0repo-config.py .
ln -s public/archives.db .
git config --global user.name 'CI'
git config --global user.email 'ci@example.com'
- name: Download incoming feed
run: curl -sSfL ${{ inputs.feed_url }} -o incoming/feed.xml
- name: Register archive
if: inputs.archive_url
run: |
name=$(basename ${{ inputs.archive_url }})
curl -sSfL ${{ inputs.archive_url }} -o incoming/$name
hash=($(sha1sum incoming/$name))
cd public
echo "$name $hash ${{ inputs.archive_url }}" >> archives.db
git add archives.db
git commit -m "Register $name"
- name: Import GPG key
run: echo "${{ secrets.GPG_KEY }}" | gpg --import -
- name: Run 0repo
run: |
curl -sSfLO https://get.0install.net/0install.sh && chmod +x 0install.sh
./0install.sh run https://apps.0install.net/0install/0repo.xml
- name: Push
run: cd public && git push
The apps.0install.net workflows are a more thoroughly factored version of the same idea (composite actions under .github/actions/), and worth reading when you scale up.
5. Wire up an app's source repo¶
Each app keeps its own template and source code together. Concretely, in the app's repo (say myapp):
myapp/
├── .github/
│ └── workflows/
│ └── build.yml
├── src/
├── build.sh
├── myapp.xml.template
└── README.md
The template's <feed-for> points at the central repo, not at the app's own GitHub Pages:
<feed-for interface="https://YOURNAME.github.io/feeds/myapp.xml"/>
The build workflow at myapp/.github/workflows/build.yml:
name: Build
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Build release archive
run: ./build.sh ${GITHUB_REF_NAME#v}
- name: Generate per-version feed
run: |
version=${GITHUB_REF_NAME#v}
curl -sSfLO https://get.0install.net/0install.sh && chmod +x 0install.sh
./0install.sh run https://apps.0install.net/0install/0template.xml \
myapp.xml.template version=$version
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
files: |
myapp-*.xml
myapp-*.tar.gz
- name: Submit feed to central repository
env:
GH_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
run: |
version=${GITHUB_REF_NAME#v}
gh workflow run --repo=YOURNAME/feeds Incoming \
-f feed_url=https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/myapp-$version.xml \
-f archive_url=https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/myapp-$version.tar.gz
PERSONAL_TOKEN is a fine-grained personal access token with Actions: write on the feeds repo. The default GITHUB_TOKEN only has permissions on the current repo, so it can't trigger workflows elsewhere.
The flow is:
- You push tag
v1.2on themyapprepo. - CI builds the archive and runs
0template myapp.xml.template version=1.2, producingmyapp-1.2.xml. - CI uploads both the feed and the archive as release assets.
- CI calls
gh workflow runon thefeedsrepo, passing the URLs of both. - The
Incomingworkflow onfeedsdownloads them, runs0repo, signs, and pushesgh-pages.
A few minutes later, https://YOURNAME.github.io/feeds/myapp.xml carries the new version. Existing users pick it up the next time their cache becomes stale.
This is exactly the pattern 0capture and 0template use to publish into apps.0install.net.
6. Local previewing¶
For changes to 0repo-config.py itself, or for hand-staged feeds, you can still run 0repo on your laptop. Tell 0repo where the working copy lives:
cd ~/Code/feeds
ln -s feeds/0repo-config.py .
ln -s public/archives.db .
mkdir incoming
0repo register
Then drop a per-version feed into incoming/ and run:
0repo
To preview the result without pushing:
0repo proxy
# in another terminal:
http_proxy=http://localhost:8080/ 0install run https://YOURNAME.github.io/feeds/myapp.xml
What's next¶
- A catalog of third-party feeds: when the upstream is someone else, you replace the source-repo CI with a
*.watch.pyscript that polls for new releases. - 0repo's README covers multi-developer setups, archive hosting, and the rest of
0repo-config.py.