diff --git a/.github/workflows/renovate-config.yml b/.github/workflows/renovate-config.yml new file mode 100644 index 0000000..ccfdb2d --- /dev/null +++ b/.github/workflows/renovate-config.yml @@ -0,0 +1,37 @@ +name: Renovate config + +on: + pull_request: + paths: + - "default.json" + - "renovate.json" + - "snippets/renovate.json" + - ".github/workflows/renovate-config.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + validate: + name: Validate config (strict) + runs-on: ubuntu-latest + permissions: + contents: read + env: + # Keep in step with the Renovate image of the in-cluster CronJob + # (FerrLabs/Infra, platform/renovate/renovate-cronjob.yaml). + RENOVATE_VERSION: "44" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: "24" + - run: | + npx --yes --package "renovate@${RENOVATE_VERSION}" \ + renovate-config-validator --strict \ + default.json renovate.json snippets/renovate.json diff --git a/.github/workflows/renovate-rebase.yml b/.github/workflows/renovate-rebase.yml deleted file mode 100644 index ed44567..0000000 --- a/.github/workflows/renovate-rebase.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Renovate rebase now - -on: - pull_request: - types: [edited] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -jobs: - dispatch: - uses: FerrLabs/.github/.github/workflows/reusable-renovate-dispatch.yml@2883414f68b4029d28d8700d2a51d070f7528111 # main - with: - runner: ubuntu-latest - secrets: inherit diff --git a/.github/workflows/renovate-selfcheck.yml b/.github/workflows/renovate-selfcheck.yml deleted file mode 100644 index cf03b5b..0000000 --- a/.github/workflows/renovate-selfcheck.yml +++ /dev/null @@ -1,181 +0,0 @@ -name: Renovate self-check - -# Guards the two things that silently broke the ferrlabs-* Kellnr lookups for -# a month (issue #205) and that nothing else notices: -# -# validate — the shared preset still parses under the *current* Renovate, -# with no removed/deprecated options. `matchPackagePatterns` -# and `matchDepPatterns` were dropped in Renovate 41 and the -# config kept "working" only because Renovate auto-migrates -# at runtime; --strict turns that into a failure. -# -# kellnr-lookup — a real Renovate run against tests/renovate/fixture, which -# reproduces a product repo's layout (crate under api/, the -# kellnr registry declared in a .cargo/config.toml Renovate -# never reads). Asserts that a ferrlabs-* crate actually -# resolves and produces an update. This is the only check -# that catches a missing allowCustomCrateRegistries or a -# hostRule whose hostType doesn't match the datasource id — -# both of which fail as a WARN plus `no-result`, never as a -# non-zero exit. -# -# Runs on any change to the Renovate config, and on a schedule 30 minutes -# after each sweep so a regression coming from Renovate itself (a datasource -# change, a registry outage, an expired CARGO_REGISTRIES_KELLNR_TOKEN) shows -# up as a red run rather than as an absence of PRs. - -on: - pull_request: - paths: - - "default.json" - - "renovate.json" - - "snippets/renovate.json" - - "tests/renovate/**" - - ".github/workflows/renovate.yml" - - ".github/workflows/renovate-selfcheck.yml" - schedule: - - cron: "30 */6 * * *" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -permissions: {} - -env: - # Keep in step with the Renovate bundled by renovatebot/github-action in - # renovate.yml — a major here should be raised when that action's major is. - # Pinned to a major (not `latest`) so an upstream breaking change surfaces - # as a deliberate bump rather than as a surprise red run. - RENOVATE_VERSION: "41" - -jobs: - validate: - name: Validate config (strict) - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - - name: Set up Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 - with: - node-version: "24" - - - name: renovate-config-validator --strict - run: | - npx --yes --package "renovate@${RENOVATE_VERSION}" \ - renovate-config-validator --strict \ - default.json renovate.json snippets/renovate.json - - kellnr-lookup: - name: Resolve a ferrlabs-* crate end to end - runs-on: ubuntu-latest - permissions: - contents: read - # Fork PRs get no secrets, so the Kellnr token would be empty and the run - # would fail for a reason unrelated to the change. The validate job above - # still covers them. - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - - name: Set up Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 - with: - node-version: "24" - - - name: Fail fast if the Kellnr token is missing - env: - CARGO_REGISTRIES_KELLNR_TOKEN: ${{ secrets.CARGO_REGISTRIES_KELLNR_TOKEN }} - run: | - if [ -z "${CARGO_REGISTRIES_KELLNR_TOKEN}" ]; then - echo "::error::CARGO_REGISTRIES_KELLNR_TOKEN is not set for this run." \ - "Renovate cannot read the private Kellnr sparse index without it." - exit 1 - fi - - # Mirrors the real sweep's Cargo configuration exactly: same registry - # URL, same hostRule, same global flag, same shared preset. If any of - # them regresses in renovate.yml or default.json, this run stops finding - # the update. - - name: Run Renovate against the fixture - working-directory: tests/renovate/fixture - env: - LOG_LEVEL: debug - RENOVATE_PLATFORM: local - RENOVATE_DRY_RUN: full - RENOVATE_ONBOARDING: "false" - RENOVATE_REQUIRE_CONFIG: optional - RENOVATE_CONFIG_FILE: ${{ github.workspace }}/default.json - RENOVATE_ALLOW_CUSTOM_CRATE_REGISTRIES: "true" - RENOVATE_CUSTOM_ENV_VARIABLES: | - { - "CARGO_REGISTRIES_KELLNR_INDEX": "sparse+https://crates.ferrlabs.com/api/v1/crates/", - "CARGO_REGISTRIES_KELLNR_TOKEN": "${{ secrets.CARGO_REGISTRIES_KELLNR_TOKEN }}", - "CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS": "cargo:token" - } - RENOVATE_HOST_RULES: | - [ - { - "matchHost": "crates.ferrlabs.com", - "hostType": "crate", - "token": "${{ secrets.CARGO_REGISTRIES_KELLNR_TOKEN }}" - } - ] - # Redirected, not tee'd: at LOG_LEVEL=debug Renovate echoes its - # resolved config, and the Kellnr token appears there verbatim (it is - # not among the fields it sanitizes). Actions would mask it on the way - # out, but there is no reason to push it through the job log at all. - # The assert step below surfaces the lines that matter. - run: | - npx --yes --package "renovate@${RENOVATE_VERSION}" renovate \ - > "${RUNNER_TEMP}/renovate.log" 2>&1 - - # always(), so that a Renovate run which exits non-zero still reports - # *why* rather than leaving a step that swallowed its own output. - - name: Assert the crate resolved - if: always() - env: - LOG: ${{ runner.temp }}/renovate.log - run: | - set -euo pipefail - fail=0 - - if [ ! -s "${LOG}" ]; then - echo "::error::Renovate produced no log — the run did not start." - exit 1 - fi - - if grep -qE 'Failed to look up crate package ferrlabs-' "${LOG}"; then - echo "::error::A ferrlabs-* crate lookup returned no-result." \ - "Kit releases are not reaching the product repos — see issue #205." - grep -E 'Failed to look up crate package ferrlabs-' "${LOG}" | sort -u - fail=1 - fi - - if grep -q 'allowCustomCrateRegistries=true is required' "${LOG}"; then - echo "::error::The crate datasource refused crates.ferrlabs.com." \ - "RENOVATE_ALLOW_CUSTOM_CRATE_REGISTRIES must be set in renovate.yml." - fail=1 - fi - - # The positive assertion. The fixture pins ferrlabs-queue to 0.1.0, - # which is the oldest published version, so a working lookup always - # yields at least one update. Its absence means the dep was skipped, - # unresolved, or filtered out by a packageRule. - if ! grep -qE 'flattened updates found:.*ferrlabs-queue' "${LOG}"; then - echo "::error::No update was proposed for ferrlabs-queue." \ - "The lookup did not resolve against the Kellnr sparse index." - grep -E 'ferrlabs-queue|skipReason' "${LOG}" | tail -20 || true - fail=1 - fi - - if [ "${fail}" -ne 0 ]; then - exit 1 - fi - echo "ok — ferrlabs-queue resolves from crates.ferrlabs.com and an update was found" diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml deleted file mode 100644 index 826b1fe..0000000 --- a/.github/workflows/renovate.yml +++ /dev/null @@ -1,303 +0,0 @@ -name: Renovate - -# Runs Renovate self-hosted under a dedicated GitHub App identity -# (`ferrlabs-renovate[bot]`) installed in the FerrLabs org. Every commit, -# PR, branch, and issue Renovate creates is attributed to the bot — not -# to whichever human happens to own the PAT. -# -# Required org-level secrets (Settings → Secrets and variables → Actions): -# - RENOVATE_APP_CLIENT_ID : GitHub App Client ID (App settings → "Client ID", e.g. Iv23li…) -# - RENOVATE_APP_PRIVATE_KEY : PEM-encoded private key (full content incl. BEGIN/END lines) -# -# To create the App: see the README in this repo or -# https://github.com/organizations/FerrLabs/settings/apps/new -# -# Required App permissions: -# Repository → Contents (R/W), Issues (R/W), Pull requests (R/W), -# Workflows (R/W), Statuses (R/W), Metadata (R), -# Members (R), Dependabot alerts (R), -# Packages (R) ← REQUIRED for @ferrlabs/* lookups on GHCR npm -# Organization → Members (R) -# Install on: All repositories under FerrLabs. -# -# About `Packages (R)`: @ferrlabs/ui, @ferrlabs/ui-astro, -# @ferrlabs/ui-foundation are published to npm.pkg.github.com (GHCR) and -# require auth to even READ. Without this permission Renovate gets 401 on -# every lookup and silently emits no PRs for internal libs — third-party -# crates / npm packages still work because they're public on crates.io / -# npmjs.org. The first run after granting the permission opens the backlog -# all at once. Verify via the bootstrap-check step below: it fails the -# workflow with an actionable message if the token can't read the registry. - -on: - schedule: - - cron: "0 */6 * * *" - workflow_dispatch: - inputs: - logLevel: - description: "Renovate log level" - required: false - default: "info" - type: choice - options: ["debug", "info", "warn", "error"] - repoFilter: - description: 'Repo filter (e.g. "FerrLabs/UI" to scan a single repo)' - required: false - default: "" - -concurrency: - group: renovate - cancel-in-progress: false - -permissions: {} - -jobs: - plan: - name: Plan shards - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.plan.outputs.matrix }} - steps: - # App token (not GITHUB_TOKEN) — needed to enumerate the private org - # repos when fanning out one shard per live *-Cloud repo. Identical - # org-wide App install + unpinned tag as the renovate job's token below: - # org-wide read is inherent to discovering the repo list, so zizmor's - # github-app / unpinned-uses are suppressed here to match that already - # accepted pattern rather than flagged as net-new on this step. - - name: Generate GitHub App token - id: app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 # zizmor: ignore[github-app,unpinned-uses] - with: - client-id: ${{ secrets.RENOVATE_APP_CLIENT_ID }} - private-key: ${{ secrets.RENOVATE_APP_PRIVATE_KEY }} - owner: FerrLabs # zizmor: ignore[github-app] - # A manual repoFilter collapses to one `manual` shard. A scheduled run - # fans out into disjoint shards, computed from the live repo list: - # - one shard per non-archived *-Cloud repo (each is a heavy - # api+web+docker monorepo, worth isolating so one slow/failing repo - # doesn't hold up the rest), - # - grouped shards for tools / libs / infra / games, - # - a `rest` catch-all owning every other non-archived repo. - # Shards are mutually exclusive. - - name: Compute shard matrix - id: plan - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - REPO_FILTER: ${{ inputs.repoFilter }} - run: | - set -euo pipefail - if [ -n "${REPO_FILTER}" ]; then - matrix=$(jq -cn --arg f "${REPO_FILTER}" '{include:[{shard:"manual",filter:$f}]}') - echo "matrix=${matrix}" >> "${GITHUB_OUTPUT}" - exit 0 - fi - - repos=$(gh api --paginate "orgs/FerrLabs/repos?per_page=100&type=all" \ - --jq '[.[] | select(.archived == false) | .name]') - - matrix=$(jq -cn \ - --argjson repos "${repos}" \ - --argjson tools '["FerrFlow","FerrVault","Benchmarks","Fixtures",".github","MCP","Changelog","Status"]' \ - --argjson libs '["Claude-Agents","actions-runner-image","UI","Kit"]' \ - --argjson infra '["Infra","Kubernetes","Docker-Runners"]' \ - --argjson games '["FerrGames-Discord","ArchVeil","RogueLite","Idler-Survivor"]' ' - def flt($xs): [$xs[] | "FerrLabs/" + .] | join(","); - ($tools + $libs + $infra + $games) as $named - | ($repos | map(select(endswith("-Cloud")))) as $cloud - | ($repos | map(select((endswith("-Cloud") | not) - and (. as $n | $named | index($n) | not)))) as $rest - | ( ($cloud | map({shard: ., filter: ("FerrLabs/" + .)})) - + [ {shard: "tools", filter: (flt($tools))}, - {shard: "libs", filter: (flt($libs))}, - {shard: "infra", filter: (flt($infra))}, - {shard: "games", filter: (flt($games))} ] - + (if ($rest | length) > 0 then [{shard: "rest", filter: (flt($rest))}] else [] end) - ) as $inc - | {include: $inc}') - echo "matrix=${matrix}" >> "${GITHUB_OUTPUT}" - - renovate: - name: Run Renovate (${{ matrix.shard }}) - needs: plan - # Shards are disjoint (a repo lands in exactly one), so nothing is - # processed twice or dropped. fail-fast off so one shard can't cancel the - # others mid-sweep. max-parallel caps concurrent Renovate legs — with one - # shard per *-Cloud repo plus the grouped shards this is ~12 legs, and - # unthrottled they'd trip GitHub's secondary rate limits. - strategy: - fail-fast: false - max-parallel: 6 - matrix: ${{ fromJSON(needs.plan.outputs.matrix) }} - # GitHub-hosted runner: free for this public repo, and the self-hosted - # `ferrlabs-k8s` group has allows_public_repositories=false (security - # default — don't expose the k8s cluster to public-repo workflows). - # Renovate only needs node + network reach to npm/crates.io/GHCR, no - # cluster-side resources. - runs-on: ubuntu-latest - # packages: read lets this job's GITHUB_TOKEN pull the private @ferrlabs/* - # GHCR npm packages (App installation tokens can't read GHCR — known - # limitation). Each package must list FerrLabs/.github under "Manage - # Actions access" for this token to be granted. - permissions: - packages: read - steps: - # Mint a short-lived (1h) installation token from the GitHub App. - # Replaces the long-lived PAT we used previously — commits and PRs - # are now attributed to `ferrlabs-renovate[bot]` instead of a human. - - name: Generate GitHub App token - id: app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 - with: - client-id: ${{ secrets.RENOVATE_APP_CLIENT_ID }} - private-key: ${{ secrets.RENOVATE_APP_PRIVATE_KEY }} - owner: FerrLabs - - - name: Checkout config - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - - name: Probe GHCR npm read access (informational) - env: - TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - status=$(curl -sS -o /dev/null -w '%{http_code}' \ - -H "Authorization: Bearer ${TOKEN}" \ - "https://npm.pkg.github.com/@ferrlabs%2Fui-astro") - if [ "${status}" = "200" ]; then - echo "ok — GITHUB_TOKEN can read @ferrlabs/* on GHCR (${status})" - else - # Downgraded to warning: this probe historically false-positived - # when the App had Packages:read granted AND the consumer repos - # plus .github were listed under "Manage Actions access" on - # every @ferrlabs/* package. App tokens vs GHCR npm have known - # rough edges; let Renovate itself decide whether it can reach - # the packages — if it can't, the next run will just skip - # @ferrlabs/* updates silently, which is recoverable. - echo "::warning::Probe returned ${status} from npm.pkg.github.com." \ - "GITHUB_TOKEN can't read @ferrlabs/* — verify each @ferrlabs/*" \ - "GHCR package lists FerrLabs/.github under Settings → Manage" \ - "Actions access, and that this job keeps permissions: packages: read." - fi - - - name: Run Renovate - uses: renovatebot/github-action@37beffda261423addd537c33f2d126df7f6ffbab # v46.2.6 - with: - token: ${{ steps.app-token.outputs.token }} - configurationFile: default.json - env: - LOG_LEVEL: ${{ inputs.logLevel || 'info' }} - RENOVATE_AUTODISCOVER: "true" - RENOVATE_AUTODISCOVER_FILTER: ${{ matrix.filter }} - RENOVATE_PLATFORM: "github" - RENOVATE_REPOSITORY_CACHE: "enabled" - # REQUIRED for the private Kellnr registry. The crate datasource - # hard-refuses any registry whose host isn't index.crates.io unless - # this is set — it bails out before issuing a single request - # (`registry.flavor !== 'crates.io' && !GlobalConfig.get(...)` in - # modules/datasource/crate). Every ferrlabs-* lookup then reports - # `no-result`, which reads like a missing crate rather than a config - # gate, and no Kit update is ever proposed. It is a globalOnly - # option, so it cannot live in default.json (that file is also - # consumed as a shared preset by every repo, where global options - # are rejected) — it has to come from the environment here. - RENOVATE_ALLOW_CUSTOM_CRATE_REGISTRIES: "true" - # Private Kellnr cargo registry, needed so Renovate's `cargo update` - # lockfile runs resolve it. The registry lives in each repo's - # api/.cargo/config.toml, but Renovate invokes cargo from the repo root - # where that subdir config isn't discovered ("registry index was not - # found: kellnr"), so it has to come from the environment. - # - # It must go through customEnvVariables rather than a plain `env:` - # entry: Renovate builds the environment of the commands it spawns from - # an allowlist (`basicEnvVars` in lib/util/exec/env.ts — CI, PATH, - # HOME, proxies, ...) plus customEnvVariables. A CARGO_* var declared - # as a step `env:` reaches the Renovate process but is stripped before - # cargo ever sees it. Nothing logs that it was dropped; the only symptom - # is cargo failing to parse the manifest. - # These mirror api/.cargo/config.toml, which cargo does not read here: - # it looks for .cargo/config.toml upwards from the cwd, not from - # --manifest-path. Both of that file's halves must be replicated — - # the registry index *and* the credential provider. - # - # global-credential-providers only restates cargo's own default, but - # relying on the default is not equivalent to declaring it: cargo - # refuses any auth-required registry unless the provider is set - # explicitly (`!global_provider_defined` in util/auth/mod.rs), and - # fails with "authenticated registries require a credential-provider" - # even when the token is valid. Keep this in sync with - # api/.cargo/config.toml in the consuming repos. - RENOVATE_CUSTOM_ENV_VARIABLES: | - { - "CARGO_REGISTRIES_KELLNR_INDEX": "sparse+https://crates.ferrlabs.com/api/v1/crates/", - "CARGO_REGISTRIES_KELLNR_TOKEN": "${{ secrets.CARGO_REGISTRIES_KELLNR_TOKEN }}", - "CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS": "cargo:token" - } - # Identity Renovate must recognise as its own so it keeps - # auto-rebasing its PRs instead of assuming a human edited them. - # Must byte-match the author GitHub attributes to the App bot when - # it commits via the API: `+[bot]@users.noreply.github.com`. - # create-github-app-token exposes no bot/user-id output (only token, - # installation-id, app-slug), so the previous `outputs.bot-id` was - # empty — the malformed author never matched the real commits and - # Renovate blocked every rebase. The bot user id (282300760) is - # immutable for this App, so it is pinned here. - RENOVATE_GIT_AUTHOR: "${{ steps.app-token.outputs.app-slug }}[bot] <282300760+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>" - RENOVATE_USERNAME: "${{ steps.app-token.outputs.app-slug }}[bot]" - # Auth for GHCR npm registry (where @ferrlabs/* live). The npm - # hostRule uses GITHUB_TOKEN (not the App token): App installation - # tokens can't read GHCR packages, but a repo's GITHUB_TOKEN can when - # the package grants FerrLabs/.github "Manage Actions access". The - # packageRule in default.json points registryUrls at npm.pkg.github.com. - # The github-tags / git-refs hostRules below keep the App token for - # cross-repo octokit calls (release notes) and the git-refs datasource - # fetching FerrLabs/Kit refs/heads/main. - # - # `hostType` must be a *datasource* id, never a manager id: hostRules - # are matched with a plain `search.hostType === rule.hostType`, and - # the datasource sets it from its own id. The Cargo one is `crate` - # (CrateDatasource.id) — `cargo` is the manager and matches nothing, - # so the Kellnr rule silently contributed no token and the sparse - # index answered 401, surfacing as the same `no-result` as above. - # - # The `docker` hostRules cover image lookups (the `tags/list` API, not - # a pull). Without them Renovate queries anonymously and shares the - # per-IP quota of a GitHub-hosted runner with everyone else on it, so - # lookups intermittently answer 429 TOOMANYREQUESTS and degrade to - # `no-result`, silently skipping the update. lscr.io is a GHCR front - # for linuxserver images and authenticates the same way. This is - # unrelated to the cluster's pull-through caches: those are wired into - # containerd via /etc/containerd/certs.d and only serve node-side - # image pulls, never metadata lookups from a hosted runner. - RENOVATE_HOST_RULES: | - [ - { - "matchHost": "npm.pkg.github.com", - "hostType": "npm", - "token": "${{ secrets.GITHUB_TOKEN }}" - }, - { - "matchHost": "github.com", - "hostType": "github-tags", - "token": "${{ steps.app-token.outputs.token }}" - }, - { - "matchHost": "github.com", - "hostType": "git-refs", - "token": "${{ steps.app-token.outputs.token }}" - }, - { - "matchHost": "crates.ferrlabs.com", - "hostType": "crate", - "token": "${{ secrets.CARGO_REGISTRIES_KELLNR_TOKEN }}" - }, - { - "matchHost": "ghcr.io", - "hostType": "docker", - "token": "${{ secrets.GITHUB_TOKEN }}" - }, - { - "matchHost": "lscr.io", - "hostType": "docker", - "token": "${{ secrets.GITHUB_TOKEN }}" - } - ] diff --git a/.github/workflows/reusable-ferrflow-release.yml b/.github/workflows/reusable-ferrflow-release.yml index 4a1c796..b9c5d41 100644 --- a/.github/workflows/reusable-ferrflow-release.yml +++ b/.github/workflows/reusable-ferrflow-release.yml @@ -28,7 +28,7 @@ on: required: false default: '' trigger-renovate: - description: 'After a real release, dispatch the org Renovate scan so downstream consumers pick up the new version.' + description: 'No effect: Renovate runs on a schedule in the cluster (FerrLabs/Infra, platform/renovate). Kept so existing callers stay valid.' type: boolean required: false default: false @@ -120,20 +120,3 @@ jobs: mode: publish dry_run: ${{ inputs.dry-run }} bot: true - - trigger-renovate: - name: Trigger Renovate scan on consumers - needs: release - if: ${{ inputs.trigger-renovate && github.event_name == 'push' && !startsWith(github.event.head_commit.message, 'chore(release):') }} - runs-on: ${{ inputs.runner != '' && inputs.runner || (github.event.repository.private && 'ferrlabs-k8s-light' || 'ubuntu-latest') }} - steps: - - name: Dispatch Renovate workflow - env: - GH_TOKEN: ${{ secrets.RENOVATE_TOKEN || secrets.FERRLABS_DISPATCH_TOKEN }} - run: | - set -euo pipefail - if [ -z "${GH_TOKEN:-}" ]; then - echo "RENOVATE_TOKEN / FERRLABS_DISPATCH_TOKEN not set — skipping Renovate trigger." - exit 0 - fi - gh workflow run renovate.yml -R FerrLabs/.github diff --git a/.github/workflows/reusable-renovate-dispatch.yml b/.github/workflows/reusable-renovate-dispatch.yml deleted file mode 100644 index 4ba0f43..0000000 --- a/.github/workflows/reusable-renovate-dispatch.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Reusable — Renovate rebase dispatch - -# Self-hosted Renovate only notices the "rebase/retry" checkbox when it next -# runs. Ticking the box just edits the PR body — nothing watches that — so with -# the */30 cron in renovate.yml you wait up to 30 minutes. This turns the edit -# into an immediate Renovate run scoped to the calling repo via `repoFilter`, -# which is why it finishes in seconds instead of sweeping the whole org. -# -# Callers trigger on `pull_request: types: [edited]` and pass `secrets: inherit`. -# -# Dispatching renovate.yml lives in FerrLabs/.github, and a repo's own -# GITHUB_TOKEN cannot reach another repo — so this mints a short-lived token -# from the Renovate GitHub App (RENOVATE_APP_CLIENT_ID / RENOVATE_APP_PRIVATE_KEY, -# the same app renovate.yml uses), scoped to the .github repo, and uses it to -# trigger renovate.yml. The app must have `actions: write` on FerrLabs/.github -# for the workflow_dispatch to succeed. -# -# This cannot loop: Renovate unticks the box once it has rebased, and that -# second `edited` event no longer matches the guard below. - -on: - workflow_call: - inputs: - runner: - description: 'Runner label. Public repos must pass a GitHub-hosted runner — the ferrlabs-k8s group sets allows_public_repositories=false.' - type: string - default: 'ferrlabs-k8s' - -concurrency: - group: reusable-renovate-dispatch-${{ github.workflow }}-${{ github.ref }} - # An idempotent dispatch that publishes nothing, so superseding an - # in-flight run is safe on any ref, tags included. - cancel-in-progress: true - -permissions: {} - -jobs: - dispatch: - name: Dispatch Renovate - if: >- - startsWith(github.head_ref, 'renovate/') && - contains(github.event.pull_request.body, '- [x] ') - runs-on: ${{ inputs.runner }} - steps: - - name: Mint a token from the Renovate GitHub App - id: app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 - with: - client-id: ${{ secrets.RENOVATE_APP_CLIENT_ID }} - private-key: ${{ secrets.RENOVATE_APP_PRIVATE_KEY }} - owner: FerrLabs - repositories: .github - permission-actions: write - - name: Trigger a repo-scoped Renovate run - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - REPO: ${{ github.repository }} - PR: ${{ github.event.pull_request.number }} - run: | - set -euo pipefail - gh workflow run renovate.yml -R FerrLabs/.github -f repoFilter="$REPO" - echo "Dispatched a Renovate run scoped to $REPO (requested from PR #$PR)." diff --git a/README.md b/README.md index 279a394..4a75877 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,10 @@ inherited automatically by any repo that does not ship its own version. | Path | Purpose | |------|---------| | `profile/README.md` | The public [organization profile](https://github.com/FerrLabs) page. | -| `.github/workflows/` | **Reusable workflows** (`reusable-ci-*`, `reusable-docker-build`, `reusable-release-rust`, `reusable-security-scan`, `reusable-sonarqube-scan`) called by repos via `workflow_call`, plus the org-wide `renovate.yml`. | +| `.github/workflows/` | **Reusable workflows** (`reusable-ci-*`, `reusable-docker-build`, `reusable-release-rust`, `reusable-security-scan`, `reusable-sonarqube-scan`) called by repos via `workflow_call`. | | `workflow-templates/` | **Starter workflows** shown under *Actions → New workflow* (CI for Astro/Go/Node/Rust, CodeQL, docker-publish, release, scorecard, pr-title, security-scan), each with its `.properties.json`. | | `snippets/` | Config files to **copy into** repos: `CODEOWNERS`, `renovate.json`, `dependabot.yml`, `deny.toml`, `typos.toml`, `.gitleaksignore`, `.trivyignore`. Not inherited; these are templates to drop in. | -| `default.json` / `renovate.json` | Shared **Renovate** config. `default.json` is loaded by `renovate.yml`; `renovate.json` is the org-wide preset. | +| `default.json` / `renovate.json` | Shared **Renovate** config. `default.json` is the global config of the self-hosted Renovate (a CronJob in FerrLabs/Infra, `platform/renovate`); `renovate.json` is the org-wide preset. | | `SECURITY.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SUPPORT.md` | Community health files inherited org-wide. | | `.github/ISSUE_TEMPLATE/`, `.github/PULL_REQUEST_TEMPLATE.md` | Default issue and PR templates inherited by repos without their own. | diff --git a/default.json b/default.json index c764726..52821b0 100644 --- a/default.json +++ b/default.json @@ -44,7 +44,7 @@ "osvVulnerabilityAlerts": true, "packageRules": [ { - "description": "FerrLabs npm packages — trust them, scan often, merge without delay. Override registry to GHCR (npm.pkg.github.com) since these packages aren't on npmjs.org. Auth comes from RENOVATE_HOST_RULES env in the runner workflow. internalChecksFilter:none skips the stability-days gate inherited from the global default. In practice this rule is belt-and-braces for the registry: each consuming repo's `.npmrc` already carries `@ferrlabs:registry=https://npm.pkg.github.com`, so the lookup resolves without it — the rule is what supplies the automerge/schedule policy.", + "description": "FerrLabs npm packages — trust them, scan often, merge without delay. Override registry to GHCR (npm.pkg.github.com) since these packages aren't on npmjs.org. Auth comes from RENOVATE_HOST_RULES set by the in-cluster runner (FerrLabs/Infra, platform/renovate/run.mjs). internalChecksFilter:none skips the stability-days gate inherited from the global default. In practice this rule is belt-and-braces for the registry: each consuming repo's `.npmrc` already carries `@ferrlabs:registry=https://npm.pkg.github.com`, so the lookup resolves without it — the rule is what supplies the automerge/schedule policy.", "matchPackageNames": [ "/^@ferrlabs\\//" ], @@ -99,7 +99,7 @@ "groupName": "FerrLabs Kit" }, { - "description": "FerrLabs Cargo crates published from Kit (`ferrlabs-*`) — same trust as @ferrlabs/ npm packages. No wait, automerge. registryUrls is a fallback: these deps declare `registry = \"kellnr\"`, which the cargo manager resolves from `.cargo/config.toml` relative to the repo root — but that file lives in each repo's `api/` subdirectory and is not discovered, so the dep would be skipped as `unknown-registry`. In practice the runner supplies CARGO_REGISTRIES_KELLNR_INDEX via RENOVATE_CUSTOM_ENV_VARIABLES, which the extractor reads first (getCargoIndexEnv); registryUrls here covers the case where that env is missing. Note that neither is sufficient on its own: the crate datasource refuses every non-crates.io registry unless allowCustomCrateRegistries=true, which is a global-only option set as RENOVATE_ALLOW_CUSTOM_CRATE_REGISTRIES in renovate.yml. Auth comes from the crates.ferrlabs.com hostRule there, which must use hostType `crate` (the datasource id) — `cargo` is the manager id and matches no hostRule.", + "description": "FerrLabs Cargo crates published from Kit (`ferrlabs-*`) — same trust as @ferrlabs/ npm packages. No wait, automerge. registryUrls is a fallback: these deps declare `registry = \"kellnr\"`, which the cargo manager resolves from `.cargo/config.toml` relative to the repo root — but that file lives in each repo's `api/` subdirectory and is not discovered, so the dep would be skipped as `unknown-registry`. In practice the runner supplies CARGO_REGISTRIES_KELLNR_INDEX via RENOVATE_CUSTOM_ENV_VARIABLES, which the extractor reads first (getCargoIndexEnv); registryUrls here covers the case where that env is missing. Note that neither is sufficient on its own: the crate datasource refuses every non-crates.io registry unless allowCustomCrateRegistries=true, which is a global-only option set as RENOVATE_ALLOW_CUSTOM_CRATE_REGISTRIES by the in-cluster runner (FerrLabs/Infra, platform/renovate/run.mjs). Auth comes from the crates.ferrlabs.com hostRule there, which must use hostType `crate` (the datasource id) — `cargo` is the manager id and matches no hostRule.", "matchManagers": [ "cargo" ], diff --git a/tests/renovate/README.md b/tests/renovate/README.md deleted file mode 100644 index e73eb88..0000000 --- a/tests/renovate/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Renovate self-check fixture - -`fixture/` is a minimal repository that reproduces the shape of a FerrLabs -product repo, as far as Renovate is concerned: - -- the crate manifest lives in `api/Cargo.toml`, not at the root; -- `api/.cargo/config.toml` declares the `kellnr` registry, which Renovate's - cargo manager does **not** discover (it reads `.cargo/config.toml` relative - to the repo root); -- one dependency is pinned to a deliberately stale `ferrlabs-*` version. - -`.github/workflows/renovate-selfcheck.yml` runs Renovate against it with -`--platform=local` and the same configuration the real sweep uses, then -asserts that the crate resolves and that an update is proposed. - -This exists because the `ferrlabs-*` lookups silently returned `no-result` -for over a month (issue #205): Kit shipped 2.x while the products stayed on -0.x, so security fixes in `ferrlabs-auth` / `ferrlabs-permissions` reached -nobody. Nothing failed — Renovate logged a `WARN` and carried on. The three -independent causes were all invisible to config validation: - -1. `allowCustomCrateRegistries` unset, so the crate datasource refused the - registry before making a request; -2. the Kellnr `hostRule` used `hostType: cargo` (a manager id) instead of - `crate` (the datasource id), so no token was attached and the sparse index - answered 401; -3. `matchDepPatterns` / `matchPackagePatterns` were removed in Renovate 41. - -Only an end-to-end lookup catches (1) and (2). The `validate` job in the same -workflow catches (3) and anything else `renovate-config-validator --strict` -knows about. - -## Updating the fixture - -Keep `ferrlabs-queue` pinned to a version that is **older** than the latest -published one — the check asserts that an update is found, so pinning it to -the current release would make the check pass vacuously. `0.1.0` is the -oldest published version and never needs bumping. diff --git a/tests/renovate/fixture/api/.cargo/config.toml b/tests/renovate/fixture/api/.cargo/config.toml deleted file mode 100644 index c57a1ab..0000000 --- a/tests/renovate/fixture/api/.cargo/config.toml +++ /dev/null @@ -1,8 +0,0 @@ -# Copy of the .cargo/config.toml every product repo carries under api/. -# Renovate's cargo manager looks for .cargo/config.toml from the repo root, -# so it never reads this file — which is exactly the condition under test. -[registries.kellnr] -index = "sparse+https://crates.ferrlabs.com/api/v1/crates/" - -[registry] -global-credential-providers = ["cargo:token"] diff --git a/tests/renovate/fixture/api/Cargo.toml b/tests/renovate/fixture/api/Cargo.toml deleted file mode 100644 index 9d6cd06..0000000 --- a/tests/renovate/fixture/api/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -# Fixture manifest — see ../../README.md. Mirrors a product repo: the crate -# lives under api/, and ferrlabs-queue is pinned to the oldest published -# version so the self-check can assert that an update is proposed. -[package] -name = "renovate-selfcheck-fixture" -version = "0.0.0" -edition = "2021" -publish = false - -[dependencies] -ferrlabs-queue = { version = "0.1.0", registry = "kellnr" }