Skip to content

feat(repo): new semantic versioning and CD process#363

Open
Charles Hudson (phobetron) wants to merge 2 commits into
mainfrom
NT-3524_semantic-versioning-continuous-deployment
Open

feat(repo): new semantic versioning and CD process#363
Charles Hudson (phobetron) wants to merge 2 commits into
mainfrom
NT-3524_semantic-versioning-continuous-deployment

Conversation

@phobetron

@phobetron Charles Hudson (phobetron) commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Replace the commitizen/cz-vinyl workflow with Commitlint-backed Conventional Commits. Commit messages now require scopes, local commit-msg validation, CI checks for PR titles and pushed main commits, and an interactive prompt for pnpm commit and plain interactive git commit.

Add Release Please manifest mode as the source of truth for independent package releases. The manifest covers all public NPM packages plus Android and Swift, uses package-specific components and tags, and keeps one shared release PR titled
chore(repo): release packages.

Bootstrap the first semantic release away from the old alpha line. The initial generated releases are forced to 1.0.0 for GA NPM packages and 1.0.0-beta.0 for Android and Swift. Package-local CHANGELOG.md seeds make this PR touch every configured release path.

Add a small Release Please runner with a native-bridge-impact plugin. Shared releasable runtime changes in the bridge/core path synthesize Android and Swift patch candidates. Real android and swift commits can still drive their own minor or major bumps.

Update publishing to run only from released package tags. NPM publishing resolves package-specific Release Please tags, sorts released packages in workspace dependency order, rewrites package READMEs for their release tag, skips already-published versions, and publishes only to GitHub Packages.

Android and Swift publishing now require optimization-android-v* and optimization-swift-v* tags. They derive versions from those tags and stamp native bridge builds with the released version.

Add native VERSION files, Release Please config, package changelog seeds, release self-checks, and package-version build helpers so published bundles can fall back to package.json versions outside release-tag builds.

Refresh CONTRIBUTING.md with the new contributor and maintainer flow: scoped commit expectations, squash-merge PR title rules, release impact guidance, Release Please ownership, publishing behavior, and troubleshooting notes.

[NT-3524]

@phobetron Charles Hudson (phobetron) force-pushed the NT-3524_semantic-versioning-continuous-deployment branch 3 times, most recently from 20ecb27 to 0e1298a Compare July 9, 2026 06:34
@phobetron Charles Hudson (phobetron) marked this pull request as ready for review July 9, 2026 06:35
Replace the `commitizen`/`cz-vinyl` workflow with
Commitlint-backed Conventional Commits. Commit messages now require
scopes, local `commit-msg` validation, CI checks for PR titles and
pushed `main` commits, and an interactive prompt for `pnpm commit`
and plain interactive `git commit`.

Add Release Please manifest mode as the source of truth for
independent package releases. The manifest covers all public NPM
packages plus Android and Swift, uses package-specific components and
tags, and keeps one shared release PR titled
`chore(repo): release packages`.

Bootstrap the first semantic release away from the old alpha line.
The initial generated releases are forced to `1.0.0` for GA NPM
packages and `1.0.0-beta.0` for Android and Swift. Package-local
`CHANGELOG.md` seeds make this PR touch every configured release path.

Add a small Release Please runner with a `native-bridge-impact`
plugin. Shared releasable runtime changes in the bridge/core path
synthesize Android and Swift patch candidates. Real `android` and
`swift` commits can still drive their own minor or major bumps.

Update publishing to run only from released package tags. NPM
publishing resolves package-specific Release Please tags, sorts
released packages in workspace dependency order, rewrites package
READMEs for their release tag, skips already-published versions, and
publishes only to GitHub Packages.

Android and Swift publishing now require `optimization-android-v*`
and `optimization-swift-v*` tags. They derive versions from those tags
and stamp native bridge builds with the released version.

Add native `VERSION` files, Release Please config, package changelog
seeds, release self-checks, and package-version build helpers so
published bundles can fall back to `package.json` versions outside
release-tag builds.

Refresh `CONTRIBUTING.md` with the new contributor and maintainer flow:
scoped commit expectations, squash-merge PR title rules, release impact
guidance, Release Please ownership, publishing behavior, and
troubleshooting notes.

[[NT-3524](https://contentful.atlassian.net/browse/NT-3524)]
@phobetron Charles Hudson (phobetron) force-pushed the NT-3524_semantic-versioning-continuous-deployment branch from 0e1298a to cd94aca Compare July 9, 2026 06:42
@@ -0,0 +1 @@
0.1.0-alpha17

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is here should not match .release-please-manifest.json file?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I missed that mobile has their own versions when I updated after alpha 18 was released.

const JIRA_BASE_URL = 'https://contentful.atlassian.net/browse'
const JIRA_FOOTER_REGEX =
/^\[\[[A-Z][A-Z0-9]+-\d+\]\(https:\/\/contentful\.atlassian\.net\/browse\/[A-Z][A-Z0-9]+-\d+\)\]$/imu
const JIRA_TICKET_REGEX = /(^|[^A-Z0-9])([A-Z][A-Z0-9]+-\d+)(?=$|[^A-Z0-9])/iu

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was considering using something like process.env.JIRA_BASE_URL instead, in case anyone wanted to fork the repo.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone forks, they could makes this change in their fork 😜 This script is something for us when working on it to make it easier to enter in a good commit message. If anyone on the outside works on it, it would be a bit weird for them to need to tie it to any Jira ticket. According to commitlint the footer should be optional, so it can be removed or omitted. Not sure what this script does if it can't find the ticket number; I didn't test it that way. Either way, it isn't anything blocking at all, just a maintainer helper.

Comment thread commitlint.config.cjs
@@ -0,0 +1,46 @@
const scopes = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think would be better to use commitlint.config.ts instead, favoring what AGENTS.md says:

Write new JavaScript/Node tooling as TypeScript whenever the repo toolchain can run it; do not add .js or .mjs scripts when a .ts script is practical.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't recall whether commitlint supports that. I know some tools we may use don't support TS configs, or have buggy behavior in TS configs. Will need to look into it.

Comment thread .husky/prepare-commit-msg
Comment on lines +9 to +10
exec </dev/tty
pnpm exec tsx scripts/prepare-commit-message.ts "$1"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would have a problem for those cases git (or the user) has already supplied a commit message, so we mustn't overwrite it. (e.g. Signed-off-by)

We should have a guard like, before pnpm exec:

if [ -s "$1" ] && grep -vq '^#' "$1"; then
  exit 0
fi

So it will skip if an earlier hook or tool already wrote a real message into $1, also grep succeeds when at least one line is not a # comment.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the script handled that. I know it skipped when I amended. Can always check this locally fairly easily.

"packages/universal/api-schemas": {
"component": "optimization-api-schemas",
"package-name": "@contentful/optimization-api-schemas",
"release-as": "1.0.0",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

release-as here is sticky — it will freeze all releases at the bootstrap version until it's removed.

Every package in this config pins "release-as" (1.0.0 for the NPM packages, 1.0.0-beta.0 for native, lines 11–78). Unlike a one-shot Release-As: commit footer, a release-as key in release-please-config.json is re-applied on every run of Release Please, not just the first. The official manifest docs are explicit about this:

"manually set next version to be "1.2.3" ignoring conventional commits. […] Note: once the release PR is merged you should either remove this or update it to a higher version. Otherwise subsequent manifest-pr runs will continue to use this version even though it was already set in the last release."

https://github.com/googleapis/release-please/blob/main/docs/manifest-releaser.md

Concretely, after the bootstrap release cuts 1.0.0:

  1. Someone lands a feat(...).
  2. The next release PR still forces release-as: 1.0.0 → identical to the already-released version → no version bump, no changelog entry.
  3. publish-npm.yaml's "Resolve unpublished targets" step sees 1.0.0 already published and skips it.

Net effect: releases silently freeze at the bootstrap version and never advance, with no error surfaced anywhere.

Notably, CONTRIBUTING.md in this same PR already describes the correct pattern — "the maintainer must use a temporary per-package release-as override before the release PR is created" — but the committed config bakes the override in permanently, and nothing in the PR removes it after bootstrap.

Requested change (pick one):

  • Preferred: remove the release-as keys from this file and instead bootstrap the first 1.0.0 via a one-shot Release-As: 1.0.0 footer on a commit / the release PR (or a temporary override PR that is reverted once the first release PR is open). This keeps the committed config on conventional-commit-derived bumps.
  • If the keys must land here for the initial cut: add an explicit, blocking follow-up step to the maintainer checklist"Immediately after the bootstrap release PR is created, open a PR deleting all release-as keys from release-please-config.json (or set each to "") so conventional-commit bumps resume." — and ideally track it so it can't be forgotten.

Per-package empty string ("release-as": "") also works to revert an individual package to conventional-commit bumping, per the same docs.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I specifically noted this in the message I left about this PR. I specifically said these would need to be removed in a follow-up ticket. This is why I don't like AI PR reviews.

@TimBeyer Tim Beyer (TimBeyer) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review — release/CD infrastructure

Four inline comments below (prerelease dist-tag, missing concurrency group, self-check not in CI, fragile commitlint imports). One additional finding that has no diff line to anchor to:

CODEOWNERS gap + stale commitizen artifacts.

  • release-please-config.json and .release-please-manifest.json — the files that decide every version bump — have no CODEOWNERS rule, while .github/**, /package.json, and /.husky/** all do. The files that control releases should require an owning reviewer.
  • CODEOWNERS still lists /.czrc (deleted in this PR) and /.czvinylrc.yaml, and .czvinylrc.yaml itself is still present on the branch even though commitizen/cz-vinyl are fully removed. Please delete the stale file and the two dead CODEOWNERS rules, and add rules for the two Release Please config files.

(The sticky release-as issue is tracked in a separate inline comment.)

inputs.publish)
run: |
pnpm -r ${{ steps.npm_package_targets.outputs.pnpm_filters }} publish --access public --no-git-checks
pnpm -r ${{ steps.unpublished_targets.outputs.pnpm_filters }} publish --access public --no-git-checks

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prerelease packages will be published under the latest dist-tag.

@contentful/optimization-react-native is configured as a beta prerelease (release-as: 1.0.0-beta.0, prerelease: true in release-please-config.json) but it is not private, so it resolves as a public npm target and flows through this publish step. This command passes no --tag.

npm's default dist-tag is latest, and npm publish has no special handling for semver prereleases — publishing 1.0.0-beta.0 with no --tag sets latest -> 1.0.0-beta.0, so a plain npm install @contentful/optimization-react-native resolves the beta. The sharper failure comes later: once RN ships a stable 1.0.0 and then a 1.1.0-beta.0, the new beta overwrites latest and silently serves a prerelease to everyone doing a plain install.

Requested change: derive the dist-tag from the version — publish prerelease versions (those whose release_version contains -) with --tag next (or beta) and only stable versions with latest. The resolved release_version is already available per target in the earlier steps, so the --tag can be selected per package.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good find.

issues: write
pull-requests: write

on:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow has no concurrency group, so two pushes to main can race.

publish-npm.yaml guards itself with a concurrency group, but this workflow does not. Two merges to main in quick succession (a normal PR merge followed by the release-PR merge, for example) can start two release-please runs concurrently, racing on the same shared release PR and on tag/release creation.

Requested change: add a serialized concurrency group, e.g.

concurrency:
  group: release-please-${{ github.ref }}
  cancel-in-progress: false

cancel-in-progress: false (not true) so an in-flight release/tag creation is never cancelled mid-run.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also a good find.

Comment thread package.json
"pm2:stop:all": "pm2 stop all",
"prepare": "husky",
"release:please": "tsx ./scripts/release-please.ts",
"release:self-check": "pnpm exec tsx ./scripts/list-npm-package-targets.ts self-check && pnpm exec tsx ./scripts/release-please.ts self-check",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

release:self-check is never run in CI.

This script wires up the deterministic self-checks in both release scripts (npm target resolution + native-bridge synthesis), and CONTRIBUTING.md tells maintainers to run it before changing Release Please config. But nothing enforces it — a broken config, tag regex, or target-resolution change would only surface at release time.

Requested change: run pnpm release:self-check in CI (either a step in the commitlint/pipeline workflow or its own tiny job). It's fast and pure, so it's cheap insurance for a system whose whole purpose is preventing bad releases.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is small and fast, being a step alongside something like the commitlint check could be OK.

@@ -0,0 +1,261 @@
import { input } from '@commitlint/prompt/lib/input.js'
import InputCustomPrompt from '@commitlint/prompt/lib/inquirer/InputCustomPrompt.js'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are unexported internal paths of @commitlint/prompt.

@commitlint/prompt/lib/input.js and @commitlint/prompt/lib/inquirer/InputCustomPrompt.js are internal module paths, and SelectableInputPrompt overrides onKeyPress2 / render — private API surface. Any @commitlint/prompt minor bump can move or rename these and silently break pnpm commit and the prepare-commit-msg hook. This is compounded by pairing a standalone inquirer@^9 + @types/inquirer@^9 against commitlint 21, which vendors its own inquirer — a source of runtime/type drift.

minimumReleaseAgeExclude pins the exact patch today, which helps, but Renovate will eventually propose a bump.

Requested change: at minimum add a comment documenting this internal-API coupling, and ideally a smoke test that exercises prepare-commit-message.ts (its self-check covers the pure helpers but not the prompt wiring) so a dependency bump fails a check instead of breaking commits locally.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this as anything that needs to be changed anytime soon. The functionality is nice, and it's only contributor/maintainer sugar, not anything that actually ships. Adding tests and such around it is a lot of overkill, and removing the functionality if we can only use it by reaching into internals would make it more annoying to use the CLI for building good commit messages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants