Automating a single feed with CI¶
The previous tutorial had you running 0template and 0publish on your machine and pushing the signed feed by hand. That works, but it means the GPG private key has to live on every machine you publish from, and "publish a release" is a checklist of commands you might occasionally get wrong.
This tutorial moves the work to GitHub Actions. When you push a tag, CI builds the archive, generates the per-version feed, signs it with a key stored as a repository secret, and updates the master feed on GitHub Pages. You keep using 0template and 0publish.
This tutorial assumes you've worked through Publishing your own app: you have a myapp.xml.template, a feed URL, and a GPG key.
By the end you will have:
myapp.xml.templatechecked into the same repo as your application source.- A
gh-pagesbranch holding the signed master feed and the public key. - A GitHub Actions workflow that publishes a new version every time you push a
v*tag.
1. Move the template into the source repo¶
Up to now the template lived in a dedicated myapp repo whose only job was hosting the feed. From here on, keep it next to the source code, the way 0capture and 0template do. Move myapp.xml.template into the source repository alongside the build script:
myapp/
├── .github/
│ └── workflows/
│ └── publish.yml
├── src/
├── build.sh
├── myapp.xml.template
└── README.md
The template still references the public feed URL on GitHub Pages, e.g. https://YOURNAME.github.io/myapp/myapp.xml. The feed URL is decoupled from where the template lives.
2. Create an orphan gh-pages branch¶
The signed master feed and the public key are served from a gh-pages branch. CI checks it out, updates it, and pushes back. Initialise it once:
git checkout --orphan gh-pages
git rm -rf .
cp ../myapp.xml . # the master feed from the previous tutorial
cp ../*.gpg . # the public key from the previous tutorial
git add myapp.xml *.gpg
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. Confirm the feed is still reachable at https://YOURNAME.github.io/myapp/myapp.xml.
3. Add the GPG key as a repository secret¶
Export the private key (the --armor output is what we'll paste):
gpg --export-secret-keys --armor YOURKEY
In Settings → Secrets and variables → Actions, create a new repository secret named GPG_KEY and paste the armored key as the value. Treat it as you would any other deployment credential.
4. Write the workflow¶
Create .github/workflows/publish.yml:
name: Publish
on:
push:
tags: ['v*']
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with: { path: source }
- uses: actions/checkout@v4
with: { path: public, ref: gh-pages }
- name: Build release archive
working-directory: source
run: ./build.sh ${GITHUB_REF_NAME#v}
- name: Import GPG key
run: echo "${{ secrets.GPG_KEY }}" | gpg --batch --import -
- name: Download 0install bootstrap script
run: curl -sSfLO https://get.0install.net/0install.sh && chmod +x 0install.sh
- name: Generate per-version feed
working-directory: source
run: |
version=${GITHUB_REF_NAME#v}
../0install.sh run https://apps.0install.net/0install/0template.xml \
myapp.xml.template version=$version
- name: Merge into master feed
run: |
version=${GITHUB_REF_NAME#v}
cd public
../0install.sh run https://apps.0install.net/0install/0publish.xml \
myapp.xml --add-from=../source/myapp-$version.xml
- name: Push gh-pages
working-directory: public
run: |
git config user.name 'CI'
git config user.email 'ci@example.com'
git add myapp.xml
git commit -m "Publish ${GITHUB_REF_NAME#v}"
git push
The workflow:
- Checks out the source on
mainand the published feed ongh-pagesinto separate directories. - Builds the release archive at the tag's version. Replace
./build.shwith whatever produces the archive your template expects (and uploads it to wherever the<archive href>points to; typically a GitHub Release attached to the same tag). - Imports the GPG key into the runner's keyring.
gpg --batchskips the passphrase prompt; the key must be exported without a passphrase, or you must also store and feed in the passphrase via a separate secret. - Runs
0templateto compute the manifest digest and stamp outmyapp-$version.xml. - Runs
0publish --add-fromto merge the per-version feed into the mastermyapp.xml. - Commits and pushes
gh-pages.
5. Tag a release¶
git tag v1.2
git push origin v1.2
Watch the workflow run in the Actions tab. When it finishes, the new version is live at https://YOURNAME.github.io/myapp/myapp.xml and existing users will pick it up the next time their cache becomes stale.
When to outgrow this¶
This setup is fine for one feed and one developer. It starts to creak when:
- You publish more than one feed and want them validated against the same policies (license set, release date present, signed by an authorised key).
- You want a public key with a stable URL independent of any single feed.
- You want a browsable directory listing or catalog at the repo root.
- Multiple people are pushing tags and you need the merge of the master feed to be a single transactional operation.
At that point, switch to 0repo. The next tutorial shows how.
Troubleshooting¶
gpg: skipped "YOURKEY": Inappropriate ioctl for device- The key is encrypted with a passphrase. Either strip the passphrase first (
gpg --passwd YOURKEY, then leave the new passphrase empty) and re-export, or pipe the passphrase in via--passphrase-fdtogether with--pinentry-mode loopback. fatal: Authentication failed for 'https://github.com/...'- The default
GITHUB_TOKENcan push to the same repo but only if the workflow haspermissions: contents: write, as above. Without it the push togh-pagesis rejected. Manifest digest sha256new=... does not match expected ...- The build produced a different archive than the one referenced in the template, usually because the build is non-reproducible or the upload is racing with the workflow. Pin tool versions and upload before running
0template.