Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/consumers.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Repos that pin the devkit; bump-consumers.yml opens one PR in each after a release. Only
# marola-dev repos: the org's fine-grained PAT cannot reach h0ffmann/ww3-gpu, whose two pins in
# .github/workflows/skills.yml are bumped by hand.
# marola-dev repos: marola-merge-bot, an org App, cannot be installed on h0ffmann/ww3-gpu, whose two
# pins in .github/workflows/skills.yml are bumped by hand.
marola-dev/marola
marola-dev/marola-app
marola-dev/marola-site
Expand Down
3 changes: 3 additions & 0 deletions .github/merge-key-allowlist
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Workflows whose jobs may name MERGE_BOT_KEY (MIP-0086 §5.6; scripts/merge_key_check.py).
.github/workflows/bump-consumers.yml
.github/workflows/release.yml
53 changes: 43 additions & 10 deletions .github/workflows/bump-consumers.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
# One pin-bump PR per consumer after a release (scripts/bump-consumers.sh). release.yml calls it
# once the release is published; dispatch it by hand to retry or to bump to an older tag.
# It reads MAROLA_BUMP_PAT (Contents, Pull requests and Workflows write on every repo in
# .github/consumers.txt), an org secret only marola-devkit may read: a PR opened with GITHUB_TOKEN
# would start no CI in the consumer, and could not reach it anyway. Not MAROLA_CROSS_REPO_PAT: ten
# workflows across the org receive that one, and with Workflows write any of them could push a
# branch whose new workflow runs with that repo's secrets (marola-dev/marola-devkit#63).
# once the release is published; dispatch it by hand to retry or to bump to an older tag. prepare
# runs nix with no key; publish holds marola-merge-bot's key and runs only checked-in scripts
# (MIP-0086 §5.6, merge-key-check). The bot's token starts CI in each consumer, which a
# GITHUB_TOKEN-opened PR would not.
name: bump consumers

on:
workflow_call:
inputs:
version: { type: string, required: true }
secrets:
MAROLA_BUMP_PAT: { required: true }
MERGE_BOT_KEY: { required: true }
workflow_dispatch:
inputs:
version: { description: "X.Y.Z, an existing tag without the v", required: true }
Expand All @@ -24,7 +22,7 @@ concurrency:
cancel-in-progress: false

jobs:
bump:
prepare:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
Expand All @@ -33,6 +31,41 @@ jobs:
- uses: actions/checkout@v7
- uses: DeterminateSystems/nix-installer-action@v23
- env:
GH_TOKEN: ${{ secrets.MAROLA_BUMP_PAT }}
VERSION: ${{ inputs.version }}
run: scripts/bump-consumers.sh "$VERSION"
run: scripts/bump-consumers.sh prepare "$VERSION" "$RUNNER_TEMP/bump"
- if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: devkit-bump
path: ${{ runner.temp }}/bump/
retention-days: 1

publish:
needs: prepare
# One consumer failing in prepare must not stop the others' PRs; the run stays red.
if: ${{ !cancelled() && needs.prepare.result != 'skipped' }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
name: devkit-bump
path: ${{ runner.temp }}/bump
- id: repos
run: echo "list=$(scripts/bump-consumers.sh names)" >>"$GITHUB_OUTPUT"
- id: bot
uses: ./.github/actions/bot-token
with:
app-id: ${{ vars.MERGE_BOT_APP_ID }}
private-key: ${{ secrets.MERGE_BOT_KEY }}
repositories: ${{ steps.repos.outputs.list }}
permission-contents: write
permission-pull-requests: write
permission-workflows: write
- env:
GH_TOKEN: ${{ steps.bot.outputs.token }}
VERSION: ${{ inputs.version }}
run: scripts/bump-consumers.sh publish "$VERSION" "$RUNNER_TEMP/bump"
4 changes: 3 additions & 1 deletion .github/workflows/gemini.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ jobs:
gemini:
if: github.event.requested_team.slug == 'gemini'
uses: ./.github/workflows/gemini-review.yml
secrets: inherit
secrets:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
PR_BOT_KEY: ${{ secrets.PR_BOT_KEY }}
with:
# The base commit under pull_request_target: a fork's head is never what runs.
devkit-ref: ${{ github.sha }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
with:
version: ${{ github.ref_name }}
secrets:
MAROLA_BUMP_PAT: ${{ secrets.MAROLA_BUMP_PAT }}
MERGE_BOT_KEY: ${{ secrets.MERGE_BOT_KEY }}
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ Each release moves `plugin.json`'s `version`, the flake package version and ever
together. A consumer adopts one by bumping its flake input, its workflow `@v…`/`devkit-ref` and its
marketplace `ref` at once.

## v0.11.0 — 2026-10-14

- `bump-consumers` runs as `prepare` (nix, no key) and `publish` (marola-merge-bot, checked-in scripts
only), so the bump PRs and their commits come from `marola-merge-bot[bot]`. `release.yml` passes
`MERGE_BOT_KEY` and `MAROLA_BUMP_PAT` is no longer read: marola-devkit must hold `MERGE_BOT_APP_ID`
and `MERGE_BOT_KEY`. `gemini.yml` passes its secrets explicitly (#80).

## v0.10.1 — 2026-10-14

- `skills-update` takes an optional secret `pr_bot_key` and opens its PR as `marola-pr-bot`; callers
Expand Down
15 changes: 7 additions & 8 deletions docs/3-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,10 @@ Once the release is published, `release.yml` calls `bump-consumers.yml`, which o
each repo of `.github/consumers.txt` on `chore/devkit-vX.Y.Z`: the flake input and its
`flake.lock` node, every devkit workflow `@v…`, `devkit-ref` and docs-lint clone, and the
marketplace `ref`; in a repo whose `REPOS.md` has the wiring markers (the umbrella), also that
block, regenerated by the new tag's `wiring` over shallow submodules. A person merges each. It uses the org secret `MAROLA_BUMP_PAT`, so the PRs are
authored by that token's owner: a fine-grained PAT owned by marola-dev with access to all its
repositories and Contents, Pull requests and Workflows write, whose secret only marola-devkit may
read. It is not `MAROLA_CROSS_REPO_PAT`, which ten workflows across the org receive: with Workflows
write, any of them could push a branch whose new workflow runs with that repo's secrets. h0ffmann/ww3-gpu is outside the org token's reach and not listed: bump
the `@v…` and `devkit-ref` in its `skills.yml` by hand. Dispatch
block, regenerated by the new tag's `wiring` over shallow submodules. A person merges each. A `prepare` job runs nix with no key and uploads one
patch per repo; a `publish` job holds the repo secret `MERGE_BOT_KEY` and runs only checked-in
scripts (`merge-key-check` enforces it), so the PRs are authored by `marola-merge-bot[bot]` and
start CI. h0ffmann/ww3-gpu is outside the App's reach and not listed: bump the `@v…` and `devkit-ref` in its `skills.yml` by hand. Dispatch
`bump-consumers.yml` by hand to retry a version: it updates the open PRs instead of adding more.
Adding a consumer is one line in `.github/consumers.txt`.

Expand Down Expand Up @@ -120,8 +118,9 @@ Never install graphify any other way, pass `--backend`, or run its installers or
## Secrets and cost

The devkit deploys nothing. `bot-token-test` reads the org's `PR_BOT_KEY` and `PR_BOT_APP_ID`;
`MAROLA_BUMP_PAT` (`bump-consumers.yml`) and `MAROLA_CROSS_REPO_PAT` (the callers) remain until
MIP-0086 row 11 retires them. Its reusable workflows take what the caller passes or the default
`bump-consumers.yml` reads the repo's `MERGE_BOT_KEY` and `MERGE_BOT_APP_ID`;
`MAROLA_CROSS_REPO_PAT` (the callers) remains until
MIP-0086 row 11 retires it. Its reusable workflows take what the caller passes or the default
`GITHUB_TOKEN`.
`setup-runners` and `gha-runner` touch a self-hosted runner only when a human runs them
([runners](4-reference_runners.md)).
Expand Down
2 changes: 1 addition & 1 deletion docs/4-reference_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ has a default, so a repo that sets none of them gets the org's behaviour.
| `MAROLA_RUNNER_PREFIX` | `setup-runners` | `marola` | The runner name prefix |
| `MAROLA_RUNNER_SOURCE` | `setup-runners` | `~/code/actions-runner`, then `~/actions-runner` | An unpacked actions-runner release to copy from |
| `MAROLA_OBSIDIAN_VAULT` | the `obsidian-vault` skill | `~/Documents/2nd-brain` | The maintainer's Obsidian vault |
| `MAROLA_BUMP_PAT` | `bump-consumers.yml` | — | Not an environment variable: the org secret, readable by marola-devkit alone, whose PAT opens the pin-bump PRs ([releases](3-development.md#releases)) |
| `MERGE_BOT_KEY`, `MERGE_BOT_APP_ID` | `bump-consumers.yml` | — | Not environment variables: the repo secret and variable of the `marola-merge-bot` App, which opens the pin-bump PRs ([releases](3-development.md#releases)) |
| `PR_BOT_KEY`, `PR_BOT_APP_ID` | a caller of `notify-umbrella` and `gemini-review` | — | Not environment variables: the org secret and variable of the `marola-pr-bot` App, passed as `notify-umbrella`'s `pr_bot_key` ([workflows](4-reference_workflows.md#notify-umbrella)) |
| `MAROLA_CROSS_REPO_PAT` | a caller of `notify-umbrella` | — | Deprecated fallback, read only without `pr_bot_key`: the repository secret a caller passes as the workflow's `token` |

Expand Down
109 changes: 86 additions & 23 deletions scripts/bump-consumers.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
#!/usr/bin/env bash
# bump-consumers: after a devkit release, one PR per repo in .github/consumers.txt on
# chore/devkit-vX.Y.Z, moving its pins (release.py --consumer), where it commits one the
# flake.lock's devkit node, and where it has one REPOS.md's wiring block (BUMP_WIRING overrides
# the new tag's `nix run …#wiring`). A re-run force-updates the branch and edits the open PR. GH_TOKEN must
# reach every repo; the PRs are authored by its owner. One repo failing doesn't stop the others.
# scripts/bump-consumers.sh X.Y.Z
# chore/devkit-vX.Y.Z, in two steps so the merge bot's key never shares a job with nix (MIP-0086
# §5.6). prepare needs no token: it clones each repo, moves its pins (release.py --consumer), its
# flake.lock's devkit node and REPOS.md's wiring block (BUMP_WIRING overrides the new tag's
# `nix run …#wiring`), commits, and writes DIR/version and, per bumped repo,
# DIR/<owner>_<repo>.patch, .base and (with a flake.lock) .lock. publish needs
# GH_TOKEN on every repo: it applies each patch to a fresh clone as the git identity it runs under,
# force-pushes the branch and opens or edits the PR. One repo failing doesn't stop the others.
# scripts/bump-consumers.sh prepare X.Y.Z DIR
# scripts/bump-consumers.sh publish X.Y.Z DIR
# scripts/bump-consumers.sh names # the consumers as repo names, comma-separated (bot-token's input)
# scripts/bump-consumers.sh --self-test
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
Expand Down Expand Up @@ -38,14 +43,15 @@ echo "\$*" >>"$t/wiring.log"
sed "s/^sub at .*/sub at \$(git -C sub rev-parse HEAD)/" "\$1" >"\$1.new"
mv "\$1.new" "\$1"
EOF
chmod +x "$t/bin/gh" "$t/bin/wiring"
printf '#!%s\necho "nix $*" >>"%s/nix.log"\n' "$BASH" "$t" >"$t/bin/nix"
chmod +x "$t/bin/gh" "$t/bin/wiring" "$t/bin/nix"
git init -q -b main "$t/sub"
git -C "$t/sub" commit -q --allow-empty -m sub
fixture() { # fixture NAME MARKERS(0|1)
fixture() { # fixture NAME MARKERS(0|1) [PINNED_VERSION]
local d="$t/src/$1"
git init -q -b main "$d"
mkdir -p "$d/docs/2-Building-marola"
echo 'inputs.marola-devkit.url = "github:marola-dev/marola-devkit/v0.0.1";' >"$d/flake.nix"
echo "inputs.marola-devkit.url = \"github:marola-dev/marola-devkit/v${3:-0.0.1}\";" >"$d/flake.nix"
git -C "$d" submodule add -q "file://$t/sub" sub
if [ "$2" = 1 ]; then
printf '# Repos\n<!-- wiring:start -->\nsub at stale\n<!-- wiring:end -->\n' >"$d/docs/2-Building-marola/REPOS.md"
Expand All @@ -56,12 +62,20 @@ EOF
git -C "$d" commit -q -m fixture
git clone -q --bare "$d" "$t/remote/a/$1"
}
run() { # run REPO [ENV=VALUE...]: one bump against the fixtures, its exit status echoed
prep() { # prep REPO [ENV=VALUE...]: exit status echoed
local repo="$1" rc=0; shift
env PATH="$t/bin:$PATH" BUMP_CONSUMERS_URL="file://$t/remote" BUMP_WIRING=wiring "$@" \
bash "$root/scripts/bump-consumers.sh" --one 9.9.9 "$repo" >/dev/null 2>"$t/err" || rc=$?
bash "$root/scripts/bump-consumers.sh" --one prepare 9.9.9 "$repo" "$t/out" >/dev/null 2>"$t/err" || rc=$?
echo "$rc"
}
pub() {
local repo="$1" rc=0; shift
env PATH="$t/bin:$PATH" BUMP_CONSUMERS_URL="file://$t/remote" GIT_AUTHOR_NAME=bot GIT_COMMITTER_NAME=bot "$@" \
bash "$root/scripts/bump-consumers.sh" --one publish 9.9.9 "$repo" "$t/out" >/dev/null 2>>"$t/err" || rc=$?
echo "$rc"
}
run() { local rc; rc="$(prep "$@")"; [ "$rc" != 0 ] || rc="$(pub "$1")"; echo "$rc"; }
mkdir -p "$t/out"
files() { git -C "$t/remote/$1" diff-tree --no-commit-id --name-only -r chore/devkit-v9.9.9 2>/dev/null | paste -sd' ' -; }

echo "-- umbrella_bump_regenerates_wiring_block --"
Expand All @@ -85,15 +99,41 @@ EOF
ok "$(files a/broken)" "" "nothing pushed"
ok "$(grep -c wiring "$t/err")" 1 "the failure names wiring"

echo "-- prepare_needs_no_token --"
fixture quiet 0
rm -f "$t/gh.log"
ok "$(prep a/quiet)" 0 "prepare succeeds"
ok "$(cat "$t/gh.log" 2>/dev/null || echo never)" never "prepare never calls gh"

echo "-- publish_runs_no_nix_or_wiring --"
rm -f "$t/wiring.log" "$t/nix.log"
ok "$(pub a/quiet)" 0 "publish succeeds"
ok "$(cat "$t/wiring.log" "$t/nix.log" 2>/dev/null || echo never)" never "publish runs neither"
ok "$(grep -c 'pr create' "$t/gh.log")" 1 "publish opens the PR"
ok "$(git -C "$t/remote/a/quiet" log -1 --format=%an chore/devkit-v9.9.9)" bot "the commit is the publisher's"

echo "-- already_pinned_writes_no_patch --"
fixture pinned 0 9.9.9
ok "$(prep a/pinned)" 0 "prepare succeeds"
ok "$(ls "$t/out" | grep -c '^a_pinned\.')" 0 "no patch for a repo already on the tag"
ok "$(pub a/pinned)" 0 "publish skips it"

[ "$fails" -eq 0 ] || { echo "bump-consumers: self-test FAILED ($fails)" >&2; exit 1; }
echo "bump-consumers: self-test ok" >&2
exit 0
fi

usage() { echo "bump-consumers: usage: bump-consumers.sh prepare|publish X.Y.Z DIR | names" >&2; exit 1; }

if [ "${1:-}" = names ]; then repos | sed 's#^marola-dev/##' | paste -sd, -; exit; fi

one=""
if [ "${1:-}" = --one ]; then one="$3"; shift; fi
version="${1:-}"; version="${version#v}"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "bump-consumers: usage: bump-consumers.sh X.Y.Z" >&2; exit 1; }
if [ "${1:-}" = --one ]; then mode="${2:-}" version="${3:-}" one="${4:-}" dir="${5:-}"
else mode="${1:-}" version="${2:-}" dir="${3:-}"; fi
version="${version#v}"
case "$mode" in prepare|publish) ;; *) usage ;; esac
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && [ -n "$dir" ] || usage
mkdir -p "$dir"; dir="$(cd "$dir" && pwd)"
tag="v$version" branch="chore/devkit-v$version"
work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT

Expand All @@ -107,10 +147,10 @@ Opened by marola-devkit's \`bump-consumers.yml\`; this repo's own CI gates it. W
MD
}

bump() {
local repo="$1" dir="$work/${1//\//_}" base lock="" regen=""
git clone -q --depth 1 "${BUMP_CONSUMERS_URL:-https://github.com}/$repo" "$dir"
cd "$dir"
prepare_one() {
local repo="$1" slug="${1//\//_}" d="$work/${1//\//_}" base lock="" regen=""
git clone -q --depth 1 "${BUMP_CONSUMERS_URL:-https://github.com}/$repo" "$d"
cd "$d"
base="$(git branch --show-current)"
git switch -q -C "$branch"
python3 "$root/scripts/release.py" --consumer . "$version" >/dev/null
Expand All @@ -137,8 +177,27 @@ bump() {
commit -qam "chore: marola-devkit $tag" \
-m "Tested: release.py --consumer moved the pins${lock:+, nix flake update marola-devkit the lock}${regen:+, wiring at $tag regenerated $repos_md}; this repo's CI gates the rest
Co-Authored-By: Claude <[email protected]>"
git format-patch -1 --binary --stdout >"$dir/$slug.patch"
printf '%s\n' "$base" >"$dir/$slug.base"
[ -z "$lock" ] || : >"$dir/$slug.lock"
echo "bump-consumers: $repo prepared" >&2
}

publish_one() {
local repo="$1" slug="${1//\//_}" d="$work/${1//\//_}" base lock="" pr
if [ ! -f "$dir/$slug.patch" ]; then
echo "bump-consumers: $repo has nothing to publish" >&2
return 0
fi
base="$(cat "$dir/$slug.base")"
[ ! -f "$dir/$slug.lock" ] || lock=1
git clone -q --depth 1 --branch "$base" "${BUMP_CONSUMERS_URL:-https://github.com}/$repo" "$d"
cd "$d"
git switch -q -C "$branch"
git am -q --keep-cr "$dir/$slug.patch"
# The patch was committed in the keyless job; the PR's commit should name the bot that pushes it.
git commit -q --amend --no-edit --reset-author
git push -q -f origin "$branch"
local pr
pr="$(gh pr list -R "$repo" --head "$branch" --state open --json number -q '.[0].number // empty')"
if [ -n "$pr" ]; then
gh pr edit "$pr" -R "$repo" --body "$(body "$lock")" >/dev/null
Expand All @@ -149,15 +208,19 @@ Co-Authored-By: Claude <[email protected]>"
}

if [ -n "$one" ]; then
bump "$one"
"${mode}_one" "$one"
exit
fi

[ -n "${GH_TOKEN:-}" ] || { echo "bump-consumers: GH_TOKEN is empty; give marola-devkit access to the org secret MAROLA_BUMP_PAT (docs/3-development.md, Releases)" >&2; exit 1; }
if [ "$mode" = prepare ]; then
printf '%s\n' "$version" >"$dir/version"
else
[ -n "${GH_TOKEN:-}" ] || { echo "bump-consumers publish: GH_TOKEN is empty; it must be marola-merge-bot's token on every consumer (MIP-0086)" >&2; exit 1; }
gh auth setup-git
fi
# Each repo in its own process: set -e does not apply inside a function called from `||`.
gh auth setup-git
failed=()
while read -r repo; do
"$0" --one "$version" "$repo" || { echo "::error::bump-consumers: $repo failed" >&2; failed+=("$repo"); }
"$0" --one "$mode" "$version" "$repo" "$dir" || { echo "::error::bump-consumers $mode: $repo failed" >&2; failed+=("$repo"); }
done < <(repos)
[ "${#failed[@]}" -eq 0 ] || { echo "bump-consumers: failed: ${failed[*]}" >&2; exit 1; }
[ "${#failed[@]}" -eq 0 ] || { echo "bump-consumers $mode: failed: ${failed[*]}" >&2; exit 1; }
Loading