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¶
The Zero Install GitHub Actions wrap the tools, so the workflow does not have to bootstrap 0install or shell out to it. Create .github/workflows/publish.yml:
name: Publish
on:
push:
tags: ['v*']
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7
with:
ref: gh-pages
path: gh-pages
- name: Determine version
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Build release archive
run: ./build.sh ${{ steps.version.outputs.version }}
- name: Generate per-version feed
id: feed
uses: 0install/github-actions/0template@v1
with:
template: myapp.xml.template
version: ${{ steps.version.outputs.version }}
- name: Merge into 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 'CI'
git config user.email 'ci@example.com'
git add -A
git commit -m "Publish ${{ steps.version.outputs.version }}"
git push
The workflow:
- Checks out the source on
mainand the published feed ongh-pagesinto separate directories. - Derives the version number from the tag name. The actions take the version as an input rather than deriving it themselves, so you can compute it however you like — from the tag, from a file in the repo, or with a tool such as GitVersion.
- Builds the release archive at that 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). - Runs
0templateto compute the manifest digest and stamp outmyapp-$version.xml. Thefeedoutput holds the path of the generated file, andarchivethe path of any archive generated alongside it. - Runs
0publish --add-fromto merge the per-version feed into the mastermyapp.xml, importing the GPG key beforehand and resigning the result with it. - Commits and pushes
gh-pages.
Tip
If your template generates the archive itself (see Generating archives), tell the 0template action where it will end up and it rewrites the relative href for you. Setting github-release is shorthand for the GitHub Release of the current tag; use archive-url for anywhere else:
- name: Generate per-version feed
id: feed
uses: 0install/github-actions/0template@v1
with:
template: source/myapp.xml.template
version: ${{ steps.version.outputs.version }}
github-release: true
You can then attach ${{ steps.feed.outputs.feed }} and ${{ steps.feed.outputs.archive }} to a GitHub Release for that tag.
Sharing gh-pages with a generated site¶
Many projects already publish something to gh-pages — API documentation, a project website — using an action such as actions-gh-pages. Such actions usually replace the entire branch on every run (force_orphan: true), which would delete a feed that CI had committed there separately.
Rather than fighting over the branch, hand the feed to the same publishing step. The public checkout and the 0publish --add-from merge stay exactly as they are; only the final "Push gh-pages" step changes. Copy the updated master feed and the public key into the directory the site generator produced, and let the publishing action commit the branch:
- name: Copy feed into the site
run: cp public/myapp.xml public/*.gpg source/site/
- name: Publish site
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ github.token }}
force_orphan: true
publish_dir: source/site
The public checkout is now only used to read the previous master feed and merge the new version into it. The generated site keeps being rebuilt from scratch on every release, while the feed and the public key are carried forward from one release to the next. TypedRest CodeGeneration publishes its API documentation and its feed this way.
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.