Tinfoil's generated release.yml reacts to a pushed git tag by
default. It can instead react to a published GitHub Release, or be
invoked directly as a reusable workflow, via :trigger. To automate
the "bump the version + update changelog + cut the tag" side of that
loop,
release-please is
the standard GitHub-native choice. This guide shows how to wire it
up for a tinfoil'ed Burrito app and documents the one critical
gotcha most people hit.
What release-please does
Release-please watches main for conventional-commit messages
(feat:, fix:, etc.), maintains a rolling "release PR" that
accumulates changelog entries and a proposed version bump, and
when you merge that PR, creates the git tag plus a GitHub Release
object.
You still get one human gate (merging the PR). Everything else is automatic.
The gotcha: GITHUB_TOKEN suppresses downstream workflows
GitHub's Actions runtime has an anti-recursion rule: resources
(tags, releases, pushes) created by a workflow that authenticated
as GITHUB_TOKEN will not trigger new workflow runs.
This affects both event-based triggers, not just the default:
| trigger | event release-please creates | fires? |
|---|---|---|
:tag_push | the v* tag | no |
:release_published | the GitHub Release | no |
Release-please creates both with GITHUB_TOKEN unless you explicitly
hand it a PAT, so with a stock setup the tag and release appear, no
release.yml run ever starts, and the release simply has no binaries.
Nothing errors. This is the single most common way to end up with a
release and no attached artifacts.
Option 4 below sidesteps the rule entirely and is the recommended choice for a stock release-please repo. Options 1-3 make the events attributable to a user instead.
Option 1: Personal Access Token (simplest)
Create a classic PAT with repo scope (or a fine-grained PAT with
contents: write, pull-requests: write, and issues: write on
the repo). Add it to the repo as a secret named COMMITTER_TOKEN,
then point release-please at it:
# .github/workflows/release-please.yml
name: Release Please
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.COMMITTER_TOKEN }}The tag is now created by your PAT's identity, which counts as a
distinct actor from GITHUB_TOKEN. release.yml fires normally.
This is the path tinfoil_demo uses.
Downsides: PATs expire (classic: manual renewal; fine-grained: up to a year), and they belong to a human account, so offboarding that human breaks releases.
Option 2: GitHub App (cleanest long-term)
Create a GitHub App with the same permissions, install it on the repo, and generate an installation token at workflow runtime:
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- uses: googleapis/release-please-action@v4
with:
token: ${{ steps.app-token.outputs.token }}Tags are now authored as <your-app>[bot]. Same "distinct actor"
effect; downstream workflows fire. No per-human expiry; install
the App on as many repos as you want.
Higher setup cost; requires App creation + install. Worth it once you have more than one repo using this pattern.
Option 3: Explicit workflow dispatch (no extra auth)
Leave release-please on GITHUB_TOKEN and have the release-please
job explicitly dispatch release.yml after creating the release:
- name: Trigger tinfoil release
if: ${{ steps.release.outputs.release_created }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.release.outputs.tag_name }}
run: gh workflow run release.yml --ref "$TAG"This is what tinfoil itself uses internally for its dogfood smoke test.
Caveat: release.yml runs on the workflow_dispatch event, not the
tag push, so the generated workflow has to accept that trigger. The
default :tag_push template only accepts on: push: tags, so this
option needs trigger: :workflow_call, which emits a
workflow_dispatch with a tag input alongside the callable entry
point.
At which point Option 4 is strictly simpler: if you are already
setting that trigger, calling the workflow with uses: is one less
moving part than dispatching it and waiting for a separate run.
Option 4: Call the workflow directly (recommended)
Instead of hoping an event propagates, have the release-please
workflow invoke tinfoil's workflow as a reusable one, in the same run.
No event is involved, so the GITHUB_TOKEN rule never applies and no
long-lived credential is needed.
tinfoil: [
targets: [:darwin_arm64, :linux_x86_64],
trigger: :workflow_call
]mix tinfoil.generate then emits:
on:
workflow_call:
inputs:
tag:
description: "Tag to build and attach assets to. Defaults to the calling workflow's release event tag."
required: false
type: string
workflow_dispatch:
inputs:
tag:
description: "Tag to build and attach assets to, e.g. v1.2.3"
required: true
type: stringAdd a job to release-please.yml that calls it:
binaries:
needs: release-please
if: ${{ needs.release-please.outputs.release_created == 'true' }}
uses: ./.github/workflows/release.yml
with:
tag: ${{ needs.release-please.outputs.tag_name }}
secrets: inheritThe binaries job then sits alongside your other post-release jobs (Hex publish, container build, deploy) instead of living at the far end of an event that may never arrive.
Two details the generated workflow handles for you, both consequences
of a workflow_call run inheriting the caller's ref:
GITHUB_REF_NAMEis the caller's branch, usuallymain, not the tag. Sotinfoil.build,tinfoil.publish,tinfoil.homebrew, andtinfoil.scoopare all invoked with an explicit--tag. For the build that is the difference between checking mix.exs againstv1.2.3and againstmain.actions/checkoutwould otherwise take that same branch and build whatever is on it now, which is not necessarily the commit the tag points at. Every checkout pinsref:to the tag.
workflow_dispatch is emitted alongside so a failed release can be
re-run from the Actions tab by typing the tag, without retagging.
The tag-push trigger cannot offer that.
Publishing still runs in --attach mode, since release-please created
the release earlier in the same run. See the next section.
Preserving the release-please changelog: attach mode
Release-please does not only create the tag. It creates the GitHub Release too, and writes the curated changelog section as the body.
With the default trigger: :tag_push, tinfoil's workflow fires on
the tag and tries to create that same release. It already exists, so
publish fails with release_already_exists_no_replace. Reaching for
--replace does work, but it deletes the release and recreates it
from commit-derived notes, discarding the changelog release-please
just wrote.
Any trigger other than :tag_push switches the publish step to
mix tinfoil.publish --attach, which looks the release up by tag and
uploads only the assets. The body, prerelease flag, and draft state
stay exactly as release-please wrote them.
Both non-default triggers give you that:
trigger: :workflow_call # Option 4, recommended
trigger: :release_published # needs Option 1, 2, or 3 for the token ruleOrdering is why attach pairs with the trigger rather than being a
standalone flag. On push: tags the workflow races release-please:
the tag lands first, so --attach would intermittently find no
release and fail. Both other triggers run only once the release object
exists, so the lookup always succeeds.
Two things to know:
:release_publishedis still subject to the token rule above. It is a workflow-triggering event like any other, so a release created by a job authenticated asGITHUB_TOKENwill not start a run. Pair it with Option 1, 2, or 3, or use:workflow_call, which has no such constraint.github: [draft: true]has no effect in attach mode. Attach never edits the release, so draft state belongs to whatever created it. The generated workflow omits--draftaccordingly.
If release-please is configured to create a draft release, the
published event does not fire until you publish it by hand. That is
a deliberate human gate, not a failure, but it does mean the binaries
appear a moment after the release does. :workflow_call is not
affected, since it does not wait on an event at all.
Full example
A minimal release-please.yml for an Elixir/Burrito CLI using a
PAT (Option 1):
name: Release Please
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.COMMITTER_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.jsonrelease-please-config.json:
{
"packages": {
".": {
"release-type": "elixir",
"changelog-path": "CHANGELOG.md",
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true
}
},
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
}.release-please-manifest.json (initial contents; release-please
updates it on each release):
{ ".": "0.1.0" }The elixir release-type teaches release-please where to bump the
version inside mix.exs.
bump-patch-for-minor-pre-major: true keeps feat: commits on a
patch bump while you are still in the 0.x range, matching
typical pre-1.0 hex convention.
Secrets checklist
For a fully automated flow on a tinfoil'ed Burrito app, you need these repo secrets configured:
| Secret | Used by | Required? |
|---|---|---|
COMMITTER_TOKEN (or App) | release-please-action | Yes (pick one auth option) |
HEX_API_KEY | mix hex.publish | If publishing a hex package |
HOMEBREW_TAP_TOKEN or HOMEBREW_TAP_DEPLOY_KEY | mix tinfoil.homebrew | If homebrew.enabled |
SCOOP_BUCKET_TOKEN or SCOOP_BUCKET_DEPLOY_KEY | mix tinfoil.scoop | If scoop.enabled |
Most projects with all four surfaces enabled end up reusing one
COMMITTER_TOKEN across release-please, Homebrew, and Scoop by
setting token_secret: "COMMITTER_TOKEN" in both the :homebrew
and :scoop blocks. That collapses the number of PATs to manage
from three to one.
End-to-end flow with release-please
- Commit with conventional-commit messages (
feat:,fix:) and push tomain. release-please.ymlruns and opens (or updates) a release PR titled "chore(main): release X.Y.Z".- Merge the release PR.
- release-please creates the
vX.Y.Ztag and a GitHub Release stub. - The tag push fires
release.yml(generated by tinfoil). - Tinfoil builds every target, uploads archives + attestations, and populates the GitHub Release with binaries + checksums.
- If enabled, Homebrew formula and Scoop manifest are pushed to their tap/bucket repos.
Step 5 is the one that only works if release-please is
authenticated as something other than GITHUB_TOKEN.