1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174# Versioning
Equalify ships as **one product from one repository**. Rather than version each
workspace independently, we keep a **single product version** that the whole
repo shares. This document is the spec for how that version is defined, bumped,
tagged, and surfaced.
## 1. Scheme
We follow [Semantic Versioning 2.0.0](https://semver.org): `MAJOR.MINOR.PATCH`.
Equalify is a deployed SaaS product, not a published library, so we interpret
the three numbers in terms of *our consumers* (API integrators, end users, and
the database/auth contract) rather than a public package API:
| Bump | When | Examples |
|-----------|----------------------------------------------------------------------------------------|----------|
| **MAJOR** | Breaking change to a consumer contract: API request/response shape, auth, a DB migration that isn't backward compatible, or a removed feature. | New required auth header; renamed GraphQL field; non-additive schema migration. |
| **MINOR** | Backward-compatible new capability. | New scan type; new dashboard chart; additive API field; additive migration. |
| **PATCH** | Backward-compatible bug fix or internal change with no consumer-visible surface change. | Fix stuck-scan clearing; perf tuning; styling; refactors; chores. |
While we are pre-1.0 in spirit (the product is still moving fast), the **root
`package.json` already declares `1.0.0`**, so we treat `1.0.0` as the baseline
and move forward from there. If we want to signal "still unstable, breaking
changes may land in minor," that is a conscious decision to make once β see
Β§7. Until then, the table above is the rule.
### Pre-release versions
Staging builds and release candidates use a pre-release suffix:
- `vX.Y.Z-rc.N` β release candidate being validated on staging before promotion.
Pre-releases sort *below* their final version (`1.4.0-rc.1` < `1.4.0`), which is
exactly what we want.
## 2. Source of truth
- **The version lives in the root [`package.json`](package.json) `version` field.**
This is the one canonical number.
- Workspace `package.json` versions (`apps/frontend`, `apps/backend`,
`services/*`, `shared/types`) are **not** maintained independently. Set them
to `"0.0.0"` and treat them as private/unpublished, or keep them in sync with
the root via the release script (Β§4). Do not hand-edit them.
- The git tag (Β§3) is the immutable record of what a version *was*.
## 3. Git tags
- Format: **`vMAJOR.MINOR.PATCH`** (e.g. `v1.4.0`), with optional `-rc.N`.
- Annotated tags only (`git tag -a`), so they carry a message and date.
- One tag per release, created on the commit that is promoted to `main`.
- **Legacy tags** (`MVP-1`, `vMVP-5.1`, `v1-rc4`, β¦) are left in place for
history but are abandoned. The first tag under this spec starts the new line
cleanly β see Β§7 for the suggested starting point.
## 4. Release flow
This maps onto the existing trunk-based deploy model
([`.github/workflows/deploy-apps.yml`](.github/workflows/deploy-apps.yml)):
`staging` deploys to the staging env, `main` deploys to production.
```
feature branch ββPRβββΆ staging ββ(validate on staging env)βββΆ main
β β
-rc.N tag vX.Y.Z tag
(optional) + CHANGELOG entry
```
A release is cut **when promoting `staging` β `main`**:
1. Decide the bump (MAJOR/MINOR/PATCH) from the commits since the last tag
(Β§5 makes this mechanical).
2. Run the release script (Β§6) on the merge commit. It:
- updates `version` in root `package.json`,
- updates the `CHANGELOG.md` (Β§5),
- commits as `chore(release): vX.Y.Z`,
- creates the annotated `vX.Y.Z` tag.
3. Push the tag. Existing deploy workflows fire from the branch push as they do
today; the tag is the record, not a new deploy trigger (unless we wire that
up later β Β§7).
## 5. Commit conventions β CHANGELOG
We already write [Conventional Commits](https://www.conventionalcommits.org)-style
subjects. Standardize on these types so bumps and changelogs can be derived:
| Type | Bump | Changelog section |
|-------------|-------|-------------------|
| `feat` | MINOR | Added / Changed |
| `fix` | PATCH | Fixed |
| `perf` | PATCH | Changed |
| `refactor` | PATCH | (omit or Changed) |
| `chore` | none | (omit) |
| `docs` | none | (omit) |
A `!` after the type/scope or a `BREAKING CHANGE:` footer forces a **MAJOR**
bump regardless of type, e.g. `feat(api)!: require auth header`.
> **Normalize `bug` β `fix`.** Several existing commits use `bug(scope): β¦`.
> Conventional Commits has no `bug` type; it is treated as `fix`. Prefer `fix`
> going forward so tooling counts it correctly.
`CHANGELOG.md` follows [Keep a Changelog](https://keepachangelog.com): newest
version on top, grouped by Added / Changed / Fixed, each entry linked to its
compare range (`vA.B.C...vX.Y.Z`).
## 6. Tooling (basic, manual)
Start with **npm's built-in `version`** plus a thin script β no new heavy deps.
```jsonc
// root package.json
"scripts": {
// bumps version, but DON'T let npm tag yet (we tag after changelog)
"release:patch": "npm version patch --no-git-tag-version",
"release:minor": "npm version minor --no-git-tag-version",
"release:major": "npm version major --no-git-tag-version"
}
```
Manual flow per release:
```bash
npm run release:minor # bumps root package.json
# edit CHANGELOG.md (or generate β see upgrade path)
git commit -am "chore(release): v$(node -p "require('./package.json').version")"
git tag -a "v$(node -p "require('./package.json').version")" -m "Release notesβ¦"
git push --follow-tags
```
## 7. Surfacing the version (so you can see what's deployed)
A version is only useful if you can read it off a running environment.
- **Frontend** β inject at build via Vite `define`, read from
`package.json`, and show it (footer + `console.info`):
```ts
// vite.config β define: { __APP_VERSION__: JSON.stringify(process.env.npm_package_version) }
```
- **Backend API** β expose a `GET /health` (or response header
`X-Equalify-Version`) returning `{ version, commit, env }`. The deploy
workflow can pass the version/short SHA in as an env var at build time.
- **Lambdas** β optional; tag the deployed function with the version or log it
on cold start for traceability.
## 8. Suggested starting point
1. Reset the new line at **`v1.0.0`** (or `v1.1.0` if you want the first release
under this spec to clearly differ from the legacy `1.0.0` in `package.json`).
2. Add `CHANGELOG.md` with an `## [Unreleased]` section.
3. Add the `release:*` scripts above.
4. Normalize commit types (`bug` β `fix`).
5. Add version surfacing (Β§7) when convenient.
## 9. Upgrade path (when manual gets old)
When cutting releases by hand becomes friction, adopt **[semantic-release](https://semantic-release.gitbook.io)**
or **[changesets](https://github.com/changesets/changesets)**:
- *semantic-release* β fully automated: on merge to `main` it reads commit
types, computes the next version, updates the changelog, tags, and (optionally)
triggers deploy. Best fit for our single-version, trunk-based model. Requires
disciplined commit messages β which the conventions in Β§5 already give us.
- *changesets* β better if we ever move to **per-package independent
versioning** (publishing `shared/types`, etc.). More ceremony than we need
today.
Recommendation: stay manual (Β§6) until release cadence is regular, then move to
semantic-release without changing this spec's scheme.
```