No description
  • JavaScript 100%
Find a file
2026-08-11 12:00:53 +00:00
.forgejo/workflows ci: node 24 on the release/test jobs — semantic-release 25 dropped node 20 2026-07-26 23:01:26 +02:00
examples docs: explain first-release 0.0.0 semantics and point example at canonical actions 2026-07-27 06:34:21 +02:00
test fix(gate): candidate tag must not look like semver — Harbor immutability captures it 2026-08-11 13:03:53 +02:00
.gitignore feat: initial commit 2026-07-24 14:34:50 +02:00
.releaserc.cjs feat!: publish @webgrip/semantic-release-config to the Forgejo registry 2026-07-24 16:38:12 +02:00
CHANGELOG.md chore(release): v1.2.2 [skip ci] 2026-08-11 12:00:53 +00:00
index.cjs fix(gate): candidate tag must not look like semver — Harbor immutability captures it 2026-08-11 13:03:53 +02:00
package-lock.json chore(release): v1.2.2 [skip ci] 2026-08-11 12:00:53 +00:00
package.json chore(release): v1.2.2 [skip ci] 2026-08-11 12:00:53 +00:00
README.md docs: explain the start-from-0.0.0 baseline seeding 2026-07-26 23:27:17 +02:00

@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_GITEA is set (the composite action sets it). Misconfiguration fails loudly instead of publishing nowhere.
  • Branches: main cuts stable releases, development cuts -rc.N prereleases. 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 stub package.json name). The v lives 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 through extraReleaseRules per repo, not into the shared defaults.
  • Changelog: always written and committed. All commit types visible; dependency bumps get their own Dependencies section so Renovate noise doesn't drown the rest.
  • Outputs: version and tag are always written to $GITHUB_OUTPUT (the composite action's contract; ignore tag if 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:

  1. 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.

  2. No artifact at the repo root. A root package matches every commit and over-releases.

  3. 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.

  4. .releaserc.cjs sits in each package directory; the action runs semantic-release once per package with that directory as cwd. CHANGELOG.md is 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 app and chart and 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_ACTIONS sniffing replaced by the hard SEMANTIC_RELEASE_GITEA requirement, and the release commit now also carries CHANGELOG.md per the shared assets.
  • Docker app (C): gains a CHANGELOG.md; loses the @semantic-release/github path entirely (dead code — GitHub never releases); release/* branches dropped for development; legacy feature/bugfix/hotfix kept alive via extraReleaseRules until commitlint retires them.