- JavaScript 100%
## [1.2.2](https://forgejo.webgrip.dev/webgrip/semantic-release-config/compare/v1.2.1...v1.2.2) (2026-08-11) |
||
|---|---|---|
| .forgejo/workflows | ||
| examples | ||
| test | ||
| .gitignore | ||
| .releaserc.cjs | ||
| CHANGELOG.md | ||
| index.cjs | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
@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: always written and committed. All commit types visible; dependency bumps get their own
Dependenciessection so Renovate noise doesn't drown the rest. - 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. |
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.
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.