Versions
StartOS uses Extended Versioning (ExVer) to manage package versions, allowing downstream maintainers to release updates without upstream changes.
Version Format
[#flavor:]<upstream>[-upstream-prerelease]:<downstream>[-downstream-prerelease]
| Component | Description | Example |
|---|---|---|
flavor | Optional variant for diverging forks | #libre: |
upstream | Upstream project version (SemVer) | 26.0.0 |
upstream-prerelease | Upstream prerelease suffix | -beta.1 |
downstream | StartOS wrapper revision | 0, 1, 2 |
downstream-prerelease | Wrapper prerelease suffix | -alpha.0, -beta.0 |
Flavor
Flavors are for diverging forks of a project that maintain separate version histories. Example: if a project forks into “libre” and “pro” editions that diverge significantly, each would have its own flavor prefix.
Note
Do NOT use flavors for hardware variants (like GPU types) – those should be handled via build configuration.
Examples
| Version String | Upstream | Downstream |
|---|---|---|
26.0.0:0 | 26.0.0 (stable) | 0 (stable) |
26.0.0:0-beta.0 | 26.0.0 (stable) | 0-beta.0 |
26.0.0-rc.1:0-alpha.0 | 26.0.0-rc.1 | 0-alpha.0 |
0.13.5:0-alpha.0 | 0.13.5 (stable) | 0-alpha.0 |
2.3.2:1-beta.0 | 2.3.2 (stable) | 1-beta.0 |
Version Ordering
Versions are compared by:
- Upstream version (most significant)
- Upstream prerelease (stable > rc > beta > alpha)
- Downstream revision
- Downstream prerelease (stable > rc > beta > alpha)
Example ordering (lowest to highest):
1.0.0-alpha.0:01.0.0-beta.0:01.0.0:0-alpha.01.0.0:0-beta.01.0.0:0(fully stable)1.0.0:1-alpha.01.0.0:11.1.0:0-alpha.0
Choosing a Version
When creating a new package:
- Select the latest stable upstream version – avoid prereleases (alpha, beta, rc) unless necessary.
- Match the Docker image tag – the version in
manifest/index.tsimages.*.source.dockerTagmust match the upstream version. - Match the git submodule – if using a submodule, check out the corresponding tag.
- Start downstream at 0 – increment only when making wrapper-only changes.
- Start downstream as alpha or beta – use
-alpha.0or-beta.0for initial releases.
Version Consistency Checklist
Ensure these all match for upstream version X.Y.Z:
- Version file exists:
startos/install/versions/vX.Y.Z.0.a0.ts - Version string matches:
version: 'X.Y.Z:0-alpha.0'in VersionInfo - Docker tag matches:
dockerTag: 'image:X.Y.Z'inmanifest/index.ts(if using pre-built image) - Git submodule checked out to
vX.Y.Ztag (if applicable)
File Structure
startos/install/versions/
├── index.ts # Exports current and historical versions
├── v1.0.0.0.a0.ts # Version 1.0.0:0-alpha.0
├── v1.0.0.0.ts # Version 1.0.0:0 (stable)
└── v1.1.0.0.a0.ts # Version 1.1.0:0-alpha.0
Version File Naming
Convert the version string to a filename:
- Replace
.and:with. - Replace
-alpha.with.a - Replace
-beta.with.b - Prefix with
v
| Version | Filename |
|---|---|
26.0.0:0-beta.0 | v26.0.0.0.b0.ts |
0.13.5:0-alpha.0 | v0.13.5.0.a0.ts |
2.3.2:1 | v2.3.2.1.ts |
Version File Template
import { VersionInfo, IMPOSSIBLE } from '@start9labs/start-sdk'
export const v_X_Y_Z_0_a0 = VersionInfo.of({
version: 'X.Y.Z:0-alpha.0',
releaseNotes: {
en_US: 'Initial release for StartOS',
es_ES: 'Version inicial para StartOS',
de_DE: 'Erstveeroffentlichung fuer StartOS',
pl_PL: 'Pierwsze wydanie dla StartOS',
fr_FR: 'Version initiale pour StartOS',
},
migrations: {
up: async ({ effects }) => {},
down: IMPOSSIBLE, // Use for initial versions or breaking changes
},
})
index.ts
export { v_X_Y_Z_0_a0 as current } from './vX.Y.Z.0.a0'
export const other = [] // Add previous versions here for migrations
Incrementing Versions
Upstream Update
When the upstream project releases a new version:
- Update git submodule to new tag
- Update
dockerTagin manifest/index.ts - Create new version file with new upstream version
- Reset downstream to 0
Wrapper-Only Changes
When making changes to the StartOS wrapper without upstream changes:
- Keep upstream version the same
- Increment downstream revision
- Create new version file
Promoting Prereleases
To promote from alpha to beta to stable:
- Create new version file without prerelease suffix (or with next stage)
- Update
index.tsto export new version as current - Move old version to
otherarray
Migrations
Migrations run when users update between versions:
migrations: {
up: async ({ effects }) => {
// Code to migrate from previous version
// Access volumes, update configs, etc.
},
down: async ({ effects }) => {
// Code to rollback (if possible)
},
}
Use IMPOSSIBLE for the down migration when:
- It is the initial version (nothing to roll back to)
- The migration involves breaking changes that cannot be reversed
migrations: {
up: async ({ effects }) => {
// Migration logic
},
down: IMPOSSIBLE,
}
Warning
Migrations are only for migrating data that is not migrated by the upstream service itself.
setupOnInit
Use sdk.setupOnInit() to run setup logic during installation, restore, or container rebuild. It receives a kind parameter:
| Kind | When it runs |
|---|---|
'install' | Fresh install |
'restore' | Restoring from backup |
null | Container rebuild (no data changes) |
Bootstrapping Config Files
Generate passwords, write initial config files, and seed stores on fresh install:
// init/seedFiles.ts
export const seedFiles = sdk.setupOnInit(async (effects, kind) => {
if (kind !== 'install') return
const secretKey = utils.getDefaultString({ charset: 'a-z,A-Z,0-9', len: 32 })
await storeJson.merge(effects, { secretKey })
await configToml.merge(effects, { /* initial config */ })
})
Creating Tasks
Tasks reference actions, so they must be created in a setupOnInit that runs after actions are registered in the init sequence:
// init/initializeService.ts
export const initializeService = sdk.setupOnInit(async (effects, kind) => {
if (kind !== 'install') return
await sdk.action.createOwnTask(effects, toggleRegistrations, 'important', {
reason: 'After creating your admin account, disable registrations.',
})
})