- JavaScript 100%
## [1.3.1](https://forgejo.webgrip.dev/webgrip/semantic-release-config/compare/v1.3.0...v1.3.1) (2026-09-22) ### Dependencies * **deps:** add renovate.json ([ |
||
|---|---|---|
| .forgejo/workflows | ||
| examples | ||
| test | ||
| .gitignore | ||
| .releaserc.cjs | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| index.cjs | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| renovate.json | ||
@webgrip/semantic-release-config
One semantic-release configuration for every Webgrip repo. Repos carry a ~3-line .releaserc.cjs; everything plugin-shaped lives here exactly once.
Non-negotiables baked in
- Forgejo is the sole release authority. GitHub is a tags-only push-mirror; it never runs semantic-release. The factory throws at load time unless
SEMANTIC_RELEASE_GITEAis set (the composite action sets it). Misconfiguration fails loudly instead of publishing nowhere. - Branches:
maincuts stable releases,developmentcuts-rc.Nprereleases. Merging development → main is the promotion act. - Tags:
v${version}in plain repos;<package-name>-v${version}in monorepos (derived by semantic-release-monorepo from the stubpackage.jsonname). Thevlives only on the git tag — artifact versions (docker tags,Chart.yaml, npm) are always bare semver. - Commit vocabulary: standard conventional commits only.
feat→ minor;fix/perf/refactor/revert→ patch;chore(deps)/build(deps)→ patch (Renovate bumps must roll out through GitOps); everything else releases nothing. Legacy types go throughextraReleaseRulesper repo, not into the shared defaults. - Changelog: written and committed by default. All commit types visible; dependency bumps get their own
Dependenciessection so Renovate noise doesn't drown the rest. A repo that promotesdevelopmentintomaincan turn the commit-back off withchangelog: false— see below. - Outputs:
versionandtagare always written to$GITHUB_OUTPUT(the composite action's contract; ignoretagif you don't need it). - Release commit:
chore(release): <tag> [skip ci]— inert on Forgejo and on the GitHub mirror.
Usage
Repos do not install anything and do not need a package.json. The composite action runs npm install --no-save --no-package-lock @webgrip/semantic-release-config@<pin> into the workspace before invoking semantic-release; this package's dependencies (semantic-release itself plus all plugins) hoist into node_modules where they resolve by name. See examples/forgejo-action-release-step.yml.
Single-artifact repo (.releaserc.cjs at the root):
const { makeConfig } = require('@webgrip/semantic-release-config');
module.exports = makeConfig({});
Monorepos
Rules:
-
Every releasable artifact lives in its own subdirectory with a stub
package.json— even helm charts, Go binaries, Rust crates:{ "name": "app", "private": true, "version": "0.0.0" }The name becomes the tag prefix (
app-v1.2.3) and the directory scopes commit filtering. Use short unscoped names; a scoped name would produce tags like@webgrip/app-v1.2.3. -
No artifact at the repo root. A root package matches every commit and over-releases.
-
Shared code that ships must live inside an artifact directory. Commits touching only files outside every package release nothing — correct for docs and CI, wrong for shipped code.
-
.releaserc.cjssits in each package directory; the action runs semantic-release once per package with that directory as cwd.CHANGELOG.mdis therefore per-package.
// app/.releaserc.cjs
const { makeConfig, dockerVerifyGate } = require('@webgrip/semantic-release-config');
module.exports = makeConfig({ monorepo: true, manifest: 'npm', verifyReleaseCmd: dockerVerifyGate() });
// chart/.releaserc.cjs
module.exports = makeConfig({ monorepo: true, manifest: 'helm' });
Options
| Option | Default | Effect |
|---|---|---|
monorepo |
false |
extends: 'semantic-release-monorepo'; tagFormat left to the package name. |
manifest |
none (tag-only) | 'npm': @semantic-release/npm (publish off) + commits package.json back. 'helm': yq bumps Chart.yaml in prepare + commits it back. 'composer': documented no-op — Packagist reads tags. |
chartPath |
'.' |
Directory containing Chart.yaml (helm only). |
npmPublish |
false |
Actually publish to the registry (npm only) — used by this package itself. |
changelog |
true |
false drops @semantic-release/changelog and takes CHANGELOG.md out of the commit-back. With no manifest to bump either, @semantic-release/git is dropped whole and the release becomes a bare tag. |
branches |
main + development/rc | Full override, e.g. to add maintenance branches. |
prepareCmd |
— | Extra prepare step, appended after the built-in manifest bump. |
verifyReleaseCmd |
— | Smoke test that runs before the tag is cut. Usually dockerVerifyGate(). |
extraReleaseRules |
[] |
Prepended, so they win over defaults. For repo-local legacy commit types. |
extraNotesTypes |
[] |
Prepended changelog section mappings. |
extraAssets |
[] |
Extra files for the release commit. |
releaseAssets |
— | Globs uploaded as Forgejo release assets (binaries, SBOMs). |
issueUrlFormat |
preset default | Only needed for external trackers (ClickUp etc.). #123 refs already link to Forgejo issues by default, since the preset derives URLs from the repo URL. |
floatingMajorTag |
false |
Force-pushes vN on stable releases (composite-action convention, like actions/checkout@v4). Skips prereleases; incompatible with monorepo. |
giteaUrl |
https://forgejo.webgrip.dev |
Escape hatch. |
dockerVerifyGate(opts?)
Returns the verifyReleaseCmd that proves the image still builds before the tag exists, so a broken Dockerfile never burns a version number. With BUILD_CACHE_REF exported by the action, it reads the buildx cache the publish path writes (unchanged Dockerfile → cache hit in seconds) and routes base images through the Harbor proxy. It never writes to the cache — the gate is amd64-only, and writing would clobber the arm64 layers publish builds into the same ref. Without the env var it falls back to a plain docker build. Options: dockerfile, context, platform, buildArgs.
changelog: false and the promotion branch model
The release commit is the reason main and development drift apart. semantic-release cuts the
stable version on main, @semantic-release/git pushes chore(release): vX.Y.Z [skip ci] there,
and that commit exists nowhere else — so every promotion has to be followed by a back-merge of
main into development. Forget it once and the next rc is computed against the last prerelease
tag instead of the stable one, which cuts a vX.Y.Z-rc.N+1 for a version that already shipped.
changelog: false removes the cause. With no changelog to commit and no manifest to bump, nothing
is pushed back to the branch and the release is the tag alone, so main after a promotion is
byte-for-byte the commit that was promoted. The release notes are unaffected — they are generated
by @semantic-release/release-notes-generator and published to the Forgejo release page either
way. What you give up is CHANGELOG.md in the working tree.
Only worth it for repos that actually run the development → main promotion. A repo releasing
straight off main has no drift to avoid and should keep the changelog.
First-release semantics (start from 0.0.0)
We start every repo from 0.0.0, so the first feat cuts 0.1.0 and the first fix cuts 0.0.1 — not semantic-release's default 1.0.0.
This can't live in this config factory. semantic-release hardcodes the first release to FIRST_RELEASE = "1.0.0" whenever no prior release exists, ignoring the bump type, and it reads the tag list into branch.tags during branch discovery — before any plugin runs — so a verifyConditions/exec step that created a tag would be seen too late. The baseline has to exist before semantic-release starts.
So the composite action seeds a v0.0.0 tag (<package-name>-v0.0.0 in monorepos) at the repo's root commit the first time it runs against a repo with no version tag. semantic-release then computes the first real version off that 0.0.0 baseline. The seed is idempotent — once any real release tag exists it's skipped — and requires full history with tags (actions/checkout fetch-depth: 0). See the Seed baseline version tag step in examples/forgejo-action-release-step.yml.
Because the baseline sits at the root commit, the first release's changelog covers the entire pre-existing history — expected, and only happens once. A breaking change before the first release still resolves to 1.0.0 (major bump from 0.0.0).
Local preview
SEMANTIC_RELEASE_GITEA=true npx semantic-release --dry-run --no-ci
Migration notes (behavior changes, per origin config)
- App+chart trains (A): name the stubs
appandchartand the tag sequences continue unchanged (app-v…,chart-v…). Prerelease channel renames-development.N→-rc.N(in-flight prereleases restart their counter). Release rules move from preset defaults to the explicit table: refactors and Renovate bumps start cutting releases. Path filtering is new: chart-only commits no longer bump the app. - ploeg (B): near-zero delta;
GITEA_ACTIONSsniffing replaced by the hardSEMANTIC_RELEASE_GITEArequirement, and the release commit now also carriesCHANGELOG.mdper the shared assets. - Docker app (C): gains a
CHANGELOG.md; loses the@semantic-release/githubpath entirely (dead code — GitHub never releases);release/*branches dropped fordevelopment; legacyfeature/bugfix/hotfixkept alive viaextraReleaseRulesuntil commitlint retires them.