Skip to content
kanso-labsPublic

About

Shared composite actions and reusable workflows for the kanso-labs organization — Node setup, workflow linting, release-please, npm trusted publishing, and @renovate commands.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

github-actions

Shared GitHub Actions workflows and composite actions for the kanso-labs organization.

Every repository here is public, so nothing needs to be configured for one of them to consume these. A private consumer would need Settings → Actions → General → Access opened up first.

What is here

Thing Kind Solves
lint-workflows Composite action Running actionlint, pinned, in every repository that has workflows
setup-node Composite action The Node setup preamble repeated in every Node CI job
upstream-changelog Composite action Getting an updated dependency's own release notes into a consumer's changelog
_publish-npm.yaml Reusable workflow Publishing a package to npm and to GitHub Packages, after a release is cut
_release-please.yaml Reusable workflow Proposing releases, with a token whose pull requests run CI
_renovate-command.yaml Reusable workflow Answering @renovate rebase on a pull request, the way Dependabot does
_dependabot-auto-merge.yaml Reusable workflow Merging Dependabot's minor and patch fixes once checks pass, as Renovate's do

Consuming them

Pin an exact version. Renovate opens the bump pull requests, the same way it does for every other action these repositories pin.

- name: Set up Node
  uses: kanso-labs/actions/setup-node@v4.0.0
concurrency:
  cancel-in-progress: false
  group: release-please

jobs:
  release-please:
    name: Propose releases
    uses: kanso-labs/actions/.github/workflows/_release-please.yaml@v4.0.0
    secrets:
      client-id: ${{ secrets.RELEASE_PLEASE_CLIENT_ID }}
      private-key: ${{ secrets.RELEASE_PLEASE_PRIVATE_KEY }}

That concurrency block is not optional — see below.

Tracking @main instead would mean a mistake here breaks CI in every consuming repository at once, with no way to hold one back. That is the whole reason for the tags.

setup-node

Reads the Node version from .tool-versions, restores the npm cache, and runs npm ci. Full inputs are in its README.

The npm cache comes from actions/setup-node's own cache: npm, which is why consumers do not need a separate actions/cache step for ~/.npm. Any such step left behind is doing nothing.

lint-workflows

Runs actionlint over .github/workflows, at a pinned version. Full inputs are in its README.

Put it in the job a ruleset already requires rather than in a job of its own. A new job means a new check name, and a check name nothing requires can fail without stopping anything — so a workflow linter added that way reports problems to no one.

A repository silences a rule by committing .github/actionlint.yaml, which actionlint discovers by itself.

One stale-list case is already handled here and needs nothing from consumers. actionlint carries its own copy of the valid permission scopes, so a scope GitHub has added since the pinned release reads as an error on a workflow that is perfectly correct — code-quality, which actions/upload-code-coverage requires. The action passes an -ignore for exactly that message, and retires it when the pin moves.

upstream-changelog

Rewrites a Renovate pull request body so that the release notes of the dependency it updates reach the changelog release-please writes at merge time — turning a release that says update dependency nzbgetcom/nzbget to v26.3 into one that lists what nzbget actually changed. Full inputs and the mechanism are in its README.

It is worth having wherever the dependency is the product: a repository packaging somebody else's application, where "what changed" means what changed upstream. In a library it is noise, because a changelog full of Storybook's release notes buries the library's own.

Two things a consumer has to do beyond calling it: give the commit-type a section in release-please-config.json, and trigger on edited as well as opened and synchronize, so that a Renovate rewrite of the body is followed by a rewrite of ours.

_publish-npm.yaml

Publishes a package once _release-please.yaml has cut a release — to npmjs.com over trusted publishing, then to GitHub Packages. One call does both.

publish:
  name: Publish to npm
  needs: release-please
  if:
    ${{ !cancelled() && needs.release-please.outputs.release_created == 'true'
    }}
  permissions:
    contents: read
    id-token: write
    packages: write
  uses: kanso-labs/actions/.github/workflows/_publish-npm.yaml@v4.2.0
  with:
    tag-name: ${{ needs.release-please.outputs.tag_name }}

Compare release_created against the string. A bare truthiness test also passes on "false", which is what that output carries when release-please runs and decides not to cut a release — so the package would be published on every merge.

Keep !cancelled() in front of it. release-please cuts releases and then updates pull requests in one step, so a failure in the second half fails Propose releases with the tag already pushed. Without a status function GitHub applies success() and skips the publish, and a re-run cannot recover it: the release half has already relabelled the pull request autorelease: tagged, so the re-run finds nothing to release. A failed call still hands its outputs to the caller, which is what makes release_created safe to read after one — measured for kanso-labs/unplugin-style-dictionary#425.

Pass tag-name, which needs v4.2.0 or later. release-please tags the release pull request's merge commit, whichever run cuts the release, while both jobs check out the commit of the run that calls them. The two differ when a later run cuts the release: the merge commit's run displaced from the caller's concurrency queue, or failing before it tagged. Publishing then would ship the later commit's code under the tagged version. Given tag-name, both jobs refuse instead and name both commits, and the version stays tagged and unpublished for a person to publish from the tag. Checking out the tag would not be enough: npm writes the run's GITHUB_SHA into the provenance attestation, which would then name a commit the tarball was not built from.

Two registries, two jobs, one order

Publish to npm runs first and Publish to GitHub Packages needs: it.

npmjs.com is the source of truth for what a version of a package is; GitHub Packages carries a copy. The edge between the jobs is what keeps the copy from getting ahead of the original: if the public publish fails, both registries stay a version behind, and the next release — or a re-run of the failed job on its own — catches them up together. Drop the edge and a failed npm publish leaves GitHub Packages serving a version npmjs.com has never heard of, which is the one state a version number cannot be reasoned about from.

The second job repeats the checkout, install and build rather than sharing the first job's. The two differ in the single thing a job cannot parameterise — its permissions — and the next section is why that difference has to be kept.

The two halves authenticate differently

npmjs.com goes over trusted publishing, so no secret is passed for it at all. id-token: write is what npm exchanges for a short-lived credential, and what mints the provenance attestation alongside it.

GitHub Packages has no trusted publishing. It takes GITHUB_TOKEN with packages: write, set as NODE_AUTH_TOKEN on the publish step alone rather than on the job. Nothing attested comes out of that half: provenance is minted during the OIDC exchange, so the tarball is attested at npmjs.com and only mirrored here.

Each job declares its own scope and neither holds the other's, which is load-bearing rather than tidy. npm decides whether to attempt an OIDC exchange by looking for the variables id-token: write injects, and GitHub Packages has no trusted publisher to exchange against — so a file-level block granting both scopes would invite npm to try something that registry cannot answer, in the publish half of a release that has already been tagged.

The caller grants all three together. It is checked against each job separately, so a scope left out still fails the run at once with that scope named, rather than being refused by a registry for a reason that reads as a registry problem.

The first publish of a package to npmjs.com cannot use this. npm configures a trusted publisher on the package page, and the package has to exist before there is a page to configure, so a new package is pushed by hand once and this takes over afterwards. Both packages in this organization were bootstrapped that way. GitHub Packages needs no equivalent: GITHUB_TOKEN creates the package on first publish, and it arrives public.

A new package on GitHub Packages arrives public, with nothing to flip

Worth stating because the opposite is widely reported, and this section said it too until the first two releases proved otherwise.

Both packages were created by their first release through this workflow and both came out visibility=public immediately, with no manual step:

$ gh api "/orgs/kanso-labs/packages?package_type=npm"
  <package>  visibility=public
  <package>  visibility=public

What makes that so is not isolated here, so do not over-generalize it. These are public repositories, the packages are scoped to the organization that owns them, and publishConfig.access is public in both manifests — which is what npm's --access reads. A private repository, or a manifest without that key, is a case this organization has no evidence about.

The check is still worth a glance after the first release that creates a package somewhere new, because visibility is a package-level setting and nothing in the run reports it. It is under the organization's Packages tab. Making a package public is one-way if it ever does come out private — a public package cannot be made private again.

The GitHub Packages job installs from npmjs.com, deliberately

Set up Node in Publish to GitHub Packages names registry-url as the public registry, which reads like a copy-paste slip. It is not one.

Handed a GitHub Packages URL, actions/setup-node writes a scoped registry line — @kanso-labs:registry=https://npm.pkg.github.com/ — because that registry serves exactly one scope. That line governs installs as much as publishes. A consumer that devDepends on another @kanso-labs package, pinned to a version that predates this job, would therefore run an npm ci that goes looking for that version on a registry that will never carry it, 404, and half-land the release: tagged, on npm, missing from the mirror.

So that job installs and builds against the public registry, then calls actions/setup-node a second time to repoint npm once nothing is left to resolve. The second call appends rather than replaces — the action drops only lines beginning with the registry key it is about to write, and a scoped key does not match the unscoped registry= line already in the file.

Writing that .npmrc line by hand instead would mean finding the file through NPM_CONFIG_USERCONFIG, which is an internal of the action. Calling the action twice asks the thing that owns the file to rewrite it.

Adopting the GitHub Packages half takes two merges, not one

A caller granting fewer scopes than a called job requests fails the run outright. So packages: write has to be on the caller before its pin moves to a version of this repository that has the second job, not in the same change:

  1. Merge packages: write into the consumer's publish job. Harmless while the pin is still on v2.x — a caller may grant more than a called workflow asks for, and the unused scope goes nowhere.
  2. Let Renovate bump the pin. That merge is what turns the second job on.

Doing it the other way round breaks the publish half of whatever release lands first.

ignore-scripts defaults to true here

The composite action defaults it to false and this defaults it to true, which is worth explaining rather than lining up.

npm runs prepare on publish as well as on install. A repository whose prepare downloads Playwright's browsers therefore pays for that download twice in each job — once installing, once as a step of the publish itself, where failing to fetch a browser aborts a release that has already been tagged. Nothing either package ships is produced by a lifecycle script; the build step is the whole of it.

Set it to false only for a package with a native dependency to compile.

Inputs

Input Default Description
build-script build npm script producing what is published
dry-run false Publish with --dry-run, uploading nothing
github-registry-url https://npm.pkg.github.com GitHub Packages registry the second job publishes to
ignore-scripts true Pass --ignore-scripts to npm ci and publish
node-version-file .tool-versions File the Node version is read from
registry-url https://registry.npmjs.org Public registry to publish to, and to install from

Pass build-script: '' for a package with nothing to build; the step is then skipped rather than running npm run with an empty argument.

There is no input for the GitHub Packages scope. That registry accepts only the owning organization's, so it is always github.repository_owner — which, inside a called workflow, is the caller's owner and not this repository's.

It inlines the Node setup rather than calling setup-node

Deliberately, and it is the one duplication here that should stay.

A ./ reference inside a reusable workflow resolves against the checkout in the workspace, and that checkout belongs to the caller — so it would look for the composite in the consuming repository and not find it. GitHub documents ./ for referencing a workflow in the same repository and says nothing either way about an action referenced from inside a called workflow, which is not a thing to rest on when every consumer publishes through here.

Naming the composite by tag would work and introduces a worse problem: it pins this repository to a version of itself that does not exist until the release carrying the change is cut.

What inlining gives up is the cache handling, and both jobs turn that off regardless — each installs once and throws the runner away, so restoring the cache costs more than the single install it would save.

Verifying a change to it

dry-run runs everything including npm publish --dry-run, in both jobs, which resolves the manifest and prints the file list without uploading. It cannot be exercised in this repository: package.json here is private: true, and npm refuses to publish — even a dry run — for a private package.

Canary it in a consumer instead, following the recipe in AGENTS.md. Pick carefully: a consumer with no @kanso-labs dependency of its own will not exercise the scoped-registry trap above, and only one that installs another kanso-labs package can.

_release-please.yaml

Wraps googleapis/release-please-action with the things that were only ever configured in one repository:

An application token, when the caller supplies one. Pull requests opened with the default GITHUB_TOKEN do not start workflow runs. Their checks sit at action_required until somebody approves them by hand, so a release stalls there — and if the repository requires status checks to merge, auto-merge can never fire, because the checks it is waiting for never report. Requests made with a GitHub App token trigger checks like any other.

Both client-id and private-key are optional. Without them the run falls back to GITHUB_TOKEN, warns in the job log, and still opens a correct release pull request — it just has to be merged by a person. That fallback is what lets a repository adopt this workflow before its secrets are in place.

There is no app-id input. Both workflows took one as a deprecated alias through v1.x and dropped it in v2.0.0, which is what makes v2 a major. A caller bumping to v2 has to rename the secret it passes; the value need not change, since a GitHub App id and a client id are interchangeable where it lands — each is accepted as the JWT issuer. Passing an app id under the client-id name still works, so the rename can be done before the secret is.

In fallback mode, nothing is merged no matter what auto-merge is set to. Merging the release pull request is only half of a release: that merge pushes the default branch, and the run it starts is what cuts the tag and the GitHub release. A push made with GITHUB_TOKEN starts no run, so merging as it leaves the release half-applied: the version is bumped and the changelog written, but nothing is tagged. The next push to the default branch, whenever that happens and whatever it is for, cuts the tag late — so on a quiet repository the released version spends an unbounded stretch being one that nobody can pin. A person's merge starts the run immediately, so the fallback leaves the merge to them.

A repository whose ruleset requires status checks cannot really use the fallback at all: the release pull request never starts the checks it is required to pass, so nobody without bypass can merge it. That is the real reason such a consumer needs the application installed, rather than a stylistic one.

Merging the release pull requests. On by default; pass auto-merge: false to turn it off. The input keeps its name, but the step merges directly rather than arming GitHub's auto-merge. It waits for the pull request's checks to finish, then lists the commits on the base branch since release-please computed the pull request. If any of them is one the changelog would list, the merge is left for the next run, by when that commit's own run has rewritten the pull request. Otherwise it merges, pinned to the head it read straight after release-please ran, and GitHub decides whether the required checks passed.

"Would list" is release-please's own rule: a type the changelog shows, or a breaking change of a type it hides, read from the changelog-sections in your release-please-config.json, or release-please's defaults if it names none.

An armed auto-merge could not refuse that way. It fires when the checks pass, whatever has landed since, so a user-facing commit could ship inside the tag with no changelog entry, and release-please would then count it as released. That happened in kanso-labs/home-assistant-applications on 2026-09-18. Requiring the pull request to be up to date with the branch would not help either: release-please leaves it behind whenever only hidden types land, and nothing would ever update it.

The step finds those pull requests by their autorelease: pending label rather than by reading the action's prs output, and that difference is what lets a caller batch its releases. prs carries only what the run created or updated: when the release release-please computes has not changed, it logs remained the same, drops that pull request, and then sets no prs output at all rather than an empty array. A run that merely re-confirms a release pull request already open therefore finds nothing there to merge, while the label is on it either way.

Permissions

The workflow deliberately declares no permissions block, because what the caller needs depends on which token it uses:

Caller supplies Needs at workflow level
client-id and private-key contents: read — the app token does the writing
Neither contents: write, issues: write, pull-requests: write

A called workflow cannot request more than its caller granted, so declaring write here would force the first kind to widen its GITHUB_TOKEN for no reason.

The caller owns the concurrency group

Overlapping release-please runs race each other. Every release merge pushes the default branch, every push starts a run, and two runs landing together try to cut the same tags and strip the same labels — one then dies on the other's half-finished work. cancel-in-progress stays off, because the last push must still get a run or the release it carries is never proposed.

The guard is required, and it goes in the caller. GitHub documents concurrency at the caller and says nothing either way about a group declared inside a called workflow, and a guard that only matters during a rare race is the worst thing to rest on undocumented behaviour: if it quietly did nothing, every consumer would lose it at once and nobody would learn that until the race happened. In the caller it is supported, it is visible in review, and forgetting it costs one repository instead of all of them.

Releasing once a day instead of once a merge

By default every merge to the default branch ends up releasing: the push run opens or updates the release pull request and merges it once its checks pass, and the next merge opens another one. To batch a day of merges into one release, keep the push trigger — it is what keeps the release pull request's changelog and version current, and what makes pending work visible all day — and merge only from a scheduled run:

on:
  push:
    branches:
      - main
  schedule:
    - cron: '0 9 * * *'
  workflow_dispatch:

jobs:
  release-please:
    name: Propose releases
    uses: kanso-labs/actions/.github/workflows/_release-please.yaml@v4.0.0
    with:
      auto-merge: ${{ github.event_name == 'schedule' }}
    secrets:
      client-id: ${{ secrets.RELEASE_PLEASE_CLIENT_ID }}
      private-key: ${{ secrets.RELEASE_PLEASE_PRIVATE_KEY }}

Evaluate github.event_name in the caller rather than asking this workflow to infer it. The caller is where the event is unambiguous, and it leaves auto-merge a mechanical input that does what its name says instead of a release policy the shared workflow imposes on every consumer at once.

workflow_dispatch proposes without merging under that expression, which is usually what you want: it is the lever for re-proposing a stuck release pull request, not a release-now button. To release before the next scheduled run, enable auto-merge on the pull request by hand.

Two things about scheduled workflows are worth knowing before relying on one.

A scheduled run starts hours after its cron, not minutes. Across the 171 scheduled release runs in this organization from 2026-08-28 to 2026-10-07, all on 0 9 * * *, the earliest started 3h11m after the cron, the median 5h03m and the latest 11h41m. So the day's release merges around midday UTC or later, while people are merging, rather than before anyone is at work. GitHub suggests moving a cron off the top of the hour to spread its load, and nothing measured here says that shortens a delay of hours. Re-measure before relying on these numbers:

gh run list --workflow release-please.yaml --event schedule --limit 100 --json createdAt

GitHub disables a schedule after 60 days without repository activity. A silent stop to releasing is the shape that failure takes.

This needs v3.2.0 or later. Before it, auto-merge read the prs output above, and a scheduled run that re-confirmed an unchanged release pull request merged nothing.

runs-on

What the job runs on, as JSON for fromJSON(): "ubuntu-latest" unless the caller says otherwise. A private repository spends the organization's included minutes on GitHub's runners, and once those are gone a job naming one fails before it starts, so no release is ever proposed. Such a caller picks a runner first and hands its answer on, as it does for its own jobs:

jobs:
  runner:
    name: Pick a runner
    uses: ./.github/workflows/_pick-runner.yaml
    secrets:
      token: ${{ secrets.RUNNER_STATUS_TOKEN }}

  release-please:
    name: Propose releases
    needs: runner
    if: ${{ !cancelled() }}
    uses: kanso-labs/actions/.github/workflows/_release-please.yaml@v4.3.0
    with:
      runs-on: ${{ needs.runner.outputs.runs-on || '["self-hosted","Linux"]' }}

The merge step needs gh and jq, which GitHub's runners carry and a self-hosted one may not. Without either it warns and leaves the release pull request open for a person to merge, which still releases.

dry-run

Runs release-please without opening a pull request or cutting a release. It exists so this workflow can be tested on a pull request, and Dry run release-please in test.yaml is its only caller. A repository releasing for real has no reason to set it.

Outputs

release_created, releases_created, paths_released, prs, tag_name and version, passed straight through from the action. prs is empty on any run that wrote no release pull request, a run that found one already correct included — read the auto-merge note above before building on it. A caller that publishes on release reads release_created, behind !cancelled() for the reason given under _publish-npm.yaml above:

publish:
  needs: release-please
  if:
    ${{ !cancelled() && needs.release-please.outputs.release_created == 'true'
    }}

_renovate-command.yaml

Renovate has no comment commands. Asking it to redo a pull request means ticking a checkbox in the body and then waiting for its next scheduled run, which in this organization is up to three hours away. This workflow closes both halves of that gap, so @renovate rebase behaves the way @dependabot rebase does.

on:
  issue_comment:
    types:
      - created

concurrency:
  cancel-in-progress: false
  group: renovate-command-${{ github.event.issue.number }}

permissions: {}

jobs:
  renovate-command:
    name: Run the command
    uses: kanso-labs/actions/.github/workflows/_renovate-command.yaml@v4.0.0
    secrets:
      client-id: ${{ secrets.RENOVATE_CLIENT_ID }}
      private-key: ${{ secrets.RENOVATE_PRIVATE_KEY }}
Comment Effect
@renovate rebase, @renovate retry, @renovate recreate Ticks the pull request's rebase checkbox, then dispatches the runner
@renovate run Dispatches the runner without touching the pull request

The three pull request commands are one command under three names, because which name is right depends on what Renovate decides to do with the branch, and nobody asking for it knows that yet. Renovate's own checkbox says "rebase/retry", and it recreates the branch when the rebase is not enough.

The bot reacts as it goes: 👀 accepted, 🚀 dispatched, 😕 refused. A refusal is never a failed run, so somebody's typo does not put a red X on the pull request.

It ticks the box rather than replacing it

There is no API for "rebase this Renovate branch", and the workflow does not try to rebase anything itself. It edits the pull request body, turning - [ ] <!-- rebase-check --> into - [x] <!-- rebase-check --> — byte for byte what GitHub records when a person clicks that checkbox — and then dispatches the runner so Renovate reads it now instead of in three hours.

Everything that decides what "rebase" means stays inside Renovate. That is the point: the workflow adds a way to ask, not a second implementation of the answer.

It follows that the commands only work on pull requests Renovate opened. On anything else the marker is absent and the run fails loudly, which is the honest outcome — the alternative is a 👍 on a request nothing will ever act on.

Both secrets are required

Unlike _release-please.yaml, there is no GITHUB_TOKEN fallback, because GITHUB_TOKEN cannot dispatch a workflow in another repository under any permissions. The app must be installed on the calling repository and on the runner, and needs actions: write there — the permission nothing else in this organization uses.

The caller grants no permissions at all. Every write goes through the app token, so this workflow can and does declare permissions: {}, which is the opposite of _release-please.yaml above and for a reason worth keeping straight: a called workflow may always request less than its caller granted. It is requesting more that fails the run.

Adopting it takes two merges, not one

issue_comment is a repository-level event. GitHub runs the copy of the workflow that is on the default branch and ignores every other copy, so a caller sitting on a branch does nothing no matter how many comments it gets — and neither does a change to one.

A repository adopting this therefore merges the caller first and finds out whether it works second. Point the uses: ref at a branch of this repository for that first merge if the workflow itself is what is being tried out, then move it to a tag.

Who may run a command

The comment author must have write or admin on the repository the comment is in. The check asks GitHub for the permission level rather than reading author_association, which is free but reports NONE for a member whose organization membership is private — silently ignoring the person most likely to be using this.

The mention must also open a line. GitHub prefixes a quoted reply with > , so quoting a command repeats it without running it.

_dependabot-auto-merge.yaml

Renovate's minor and patch updates merge themselves: the shared preset turns GitHub's auto-merge on, and the pull request squash-merges once the required checks pass. Dependabot has no such setting. Nothing in this organization configures version updates for it, so what it opens are security fixes — often for a dependency Renovate cannot reach, one that only appears in a lock file — and those were the pull requests left waiting for a person. This workflow turns auto-merge on for them, under the same rule.

name: Dependabot Auto-merge

on:
  pull_request:

permissions: {}

jobs:
  dependabot-auto-merge:
    name: Enable auto-merge
    if: github.actor == 'dependabot[bot]'
    permissions:
      contents: write
      pull-requests: write
    uses: kanso-labs/actions/.github/workflows/_dependabot-auto-merge.yaml@v4.1.0

The if: on the caller only spares every other pull request a skipped job. The workflow checks the same thing itself, and it is the check that counts.

Minor and patch only

dependabot/fetch-metadata reads the update off the pull request's own commits, and only a version-update:semver-minor or version-update:semver-patch turns auto-merge on — the rule the shared Renovate preset applies to Renovate's. For a group, the update type is the largest change in it, so one major anywhere holds the whole group. A run that schedules nothing says so in a notice, so a green check is never read as a merge on its way.

There is no waiting period. These are security fixes, and holding one back for days defeats the point of it.

Auto-merge waits for the required checks, so it can only ever merge what would have passed anyway. A consumer whose Lint checks the pull request title with commitlint has to give Dependabot a title that passes, through the commit-message settings in its own .github/dependabot.yml — otherwise the pull request waits for ever on a check that only a person can fix.

It merges with GITHUB_TOKEN, and the merge starts no workflow

A Dependabot-triggered run gets a read-only GITHUB_TOKEN and the caller's permissions raise it, but it can read Dependabot secrets only — not the organization's Actions secrets, where the application keys live. Rather than copying a private key with write access into a second store, the workflow uses GITHUB_TOKEN, and that has one consequence worth knowing: a push made with it starts no workflow run. So the merge commit runs no CI on the default branch, and does not refresh the release pull request, until something else pushes. A caller releasing on the daily schedule _release-please.yaml documents loses nothing by it — that run reads the default branch whatever pushed it, and the release pull request runs its own checks before anything ships.

It also rules out a merge queue, which GITHUB_TOKEN cannot add a pull request to. The consumer needs Settings → General → Allow auto-merge turned on, and squash merging allowed.

Adopting it takes two merges, not one

A Dependabot pull request runs the workflows its merge ref holds, and since Dependabot never edits a workflow here, those are the default branch's copies. A caller sitting on a branch does nothing for Dependabot, so a repository adopting this merges the caller first and finds out whether it works second. Point the uses: ref at a branch of this repository for that first merge if the workflow itself is what is being tried out, then move it to a tag.

A pull request Dependabot opened before the caller merged runs it on its next event: commenting @dependabot rebase is the one that is both Dependabot's own and in your hands.

Releasing this repository

release-please owns version.txt and .release-please-manifest.json. Nobody edits either by hand.

The type of the pull request title decides what happens, and because pull requests are squash-merged, that title is the only commit that reaches main:

Title type Effect
feat Minor release
fix Patch release
deps Patch release
ci Patch release
anything else No release

deps is not a Conventional Commits type. It exists because Renovate's default type is chore(deps), and chore is hidden in release-please's defaults — release-please decides there are no user-facing commits and opens no release pull request at all. So a run of nothing but dependency upgrades released nothing, and an upgrade shipped only when a feature happened to land beside it.

Consumers pin exact tags, so an upgrade that cuts no release is one nobody can pin. .github/renovate.json therefore sets semanticCommitType: deps, and release-please-config.json gives that type a visible Dependencies section. The section list there replaces release-please's defaults wholesale rather than extending them, so dropping feat or fix from it would silently stop those releases too.

ci is a Conventional Commits type, and it is visible here for the same reason deps is. The workflows and composite actions in this repository are the thing consumers pin, so a ci: commit is usually a change to the product rather than to the scaffolding around it — and hidden, it shipped only when something else happened to release beside it.

The type is coarser than the distinction it is standing in for. This repository's own lint.yaml, test.yaml and release-please.yaml are consumed by nobody, so a ci: change to one of them now cuts a release that says nothing to any consumer. release-please keys on the commit type and not the path, so that is the cost of the type being visible at all. Reach for chore: when a change genuinely touches only this repository's own scaffolding.

A plain chore: still publishes nothing, which is the point: housekeeping should not cut a release.

A release that changes how a consumer must call something is a breaking change, and it needs ! so the major moves. Consumers pin exact versions, so nothing breaks the moment it is released — but a Renovate bump landing on a repository that never read the changelog is the same failure a day later.

This repository's own release-please.yaml references ./ rather than pinning a tag, which is the one place the advice above is inverted. A pin makes the repository a consumer of itself: Renovate bumps it, the bump cuts a patch release as any deps change does, the release moves the tag, and the next Renovate run bumps it again — forever. Consumers have no such cycle, because a release of this repository does not move any tag they pin.

About

Shared composite actions and reusable workflows for the kanso-labs organization — Node setup, workflow linting, release-please, npm trusted publishing, and @renovate commands.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages