Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ jobs:
decide node "$NODE"
decide java "$JAVA"

versions:
name: Version consistency
runs-on: ubuntu-latest
# Unfiltered: the version spans every suite, and the check costs seconds.
steps:
- name: Check out repository
uses: actions/checkout@v7.0.1

- name: Every manifest agrees on one version
run: node scripts/version.mjs --check

rust:
name: Rust
needs: changes
Expand Down Expand Up @@ -124,7 +135,7 @@ jobs:
ci-status:
name: CI status
if: always()
needs: [changes, rust, python, node, java]
needs: [changes, versions, rust, python, node, java]
runs-on: ubuntu-latest
steps:
- name: Check that no required job failed
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/publish-java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ jobs:
echo "::error::Invalid version format: ${version}"
exit 1
fi
gradle_ver=$(grep -oP '^version = "\K[^"]+' sdks/java/build.gradle.kts)
if [ "$gradle_ver" != "$version" ]; then
echo "::error::Resolved ${version} != sdks/java/build.gradle.kts ${gradle_ver}"
node scripts/version.mjs --check
repo_version="$(node scripts/version.mjs --current)"
if [ "$repo_version" != "$version" ]; then
echo "::error::Resolved ${version} but the repo declares ${repo_version}"
exit 1
fi
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
Expand Down
12 changes: 7 additions & 5 deletions .github/workflows/publish-node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,14 @@ jobs:
fi
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"

- name: Verify version matches package.json
- name: Verify version matches codebase
env:
VERSION: ${{ steps.resolve.outputs.version }}
run: |
version="${{ steps.resolve.outputs.version }}"
pkg_ver=$(node -p "require('./sdks/node/package.json').version")
if [ "$pkg_ver" != "$version" ]; then
echo "::error::Resolved version ${version} != sdks/node/package.json ${pkg_ver}"
node scripts/version.mjs --check
repo_version="$(node scripts/version.mjs --current)"
if [ "$repo_version" != "$VERSION" ]; then
echo "::error::Resolved ${VERSION} but the repo declares ${repo_version}"
exit 1
fi

Expand Down
14 changes: 6 additions & 8 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,13 @@ jobs:
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"

- name: Verify version matches codebase
env:
VERSION: ${{ steps.resolve.outputs.version }}
run: |
version="${{ steps.resolve.outputs.version }}"
pyproject_ver=$(python3 -c "
import re, pathlib
m = re.search(r'^version\s*=\s*\"(.+?)\"', pathlib.Path('sdks/python/pyproject.toml').read_text(), re.M)
print(m.group(1))
")
if [ "$pyproject_ver" != "$version" ]; then
echo "::error::Input version ${version} != pyproject.toml version ${pyproject_ver}"
node scripts/version.mjs --check
repo_version="$(node scripts/version.mjs --current)"
if [ "$repo_version" != "$VERSION" ]; then
echo "::error::Requested ${VERSION} but the repo declares ${repo_version}"
exit 1
fi

Expand Down
11 changes: 11 additions & 0 deletions scripts/sync-changelog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

if (process.argv.includes("--help") || process.argv.includes("-h")) {
console.log(`Regenerate the docs changelog page and version constant from CHANGELOG.md.

usage: node scripts/sync-changelog.mjs

Takes no options. Writes docs/content/docs/resources/changelog.mdx and
docs/app/lib/version.ts; both are generated — edit CHANGELOG.md instead.
Runs automatically before \`docs\` dev/build.`);
process.exit(0);
}

const repoRoot = new URL("../", import.meta.url);
const source = fileURLToPath(new URL("CHANGELOG.md", repoRoot));
const target = fileURLToPath(
Expand Down
173 changes: 173 additions & 0 deletions scripts/version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#!/usr/bin/env node
// One version for the whole repo: `[workspace.package].version` in the root
// Cargo.toml. Everything else either derives it natively — the seven crates via
// `version.workspace`, the Python wheel via maturin, the Gradle subprojects via
// gradle.properties — or is mirrored here.
//
// node scripts/version.mjs --check verify nothing has drifted (CI gate)
// node scripts/version.mjs --current print the version the repo declares
// node scripts/version.mjs --set 0.21.0 bump the source and every mirror
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

const repoRoot = new URL("../", import.meta.url);
const abs = (relative) => fileURLToPath(new URL(relative, repoRoot));
const read = (relative) => readFileSync(abs(relative), "utf8");

const SEMVER = /^\d+\.\d+\.\d+[\w.-]*$/;

// The canonical declaration. Every mirror below is rewritten to match it.
const SOURCE = {
file: "Cargo.toml",
pattern: /^version = "(.+?)"$/m,
label: "workspace.package",
};

// Manifests with no way to reference the Cargo version natively.
const MIRRORS = [
{
file: "sdks/node/package.json",
pattern: /^( "version": ")(.+?)(",)$/m,
label: "npm package",
},
{
file: "sdks/java/gradle.properties",
pattern: /^(version=)(.+)()$/m,
label: "Gradle projects",
},
{
file: "sdks/python/taskito/__init__.py",
pattern: /^( __version__ = ")(.+?)(")$/m,
label: "Python source-tree fallback",
},
];

// Checked, never written: release notes are authored by hand, but shipping a
// version with no section of its own is a mistake worth failing CI over.
const CHANGELOG = {
file: "CHANGELOG.md",
pattern: /^## (\d+\.\d+\.\d+[\w.-]*)$/m,
label: "latest CHANGELOG section",
};

// Files that must keep deriving the version instead of restating it — a
// hardcoded literal here would silently win over the source.
function guards() {
const crates = readdirSync(abs("crates")).map((crate) => ({
file: `crates/${crate}/Cargo.toml`,
pattern: /^version = "/m,
hint: "use `version.workspace = true`",
}));
return [
...crates,
{
file: "sdks/python/pyproject.toml",
pattern: /^version = "/m,
hint: 'use `dynamic = ["version"]` so maturin reads Cargo.toml',
},
...["", "spring/", "processor/", "test-support/", "graalvm-smoke/"].map(
(project) => ({
file: `sdks/java/${project}build.gradle.kts`,
pattern: /^version = "/m,
hint: "set `version` in sdks/java/gradle.properties",
}),
),
];
}

// Reads the single capture the pattern is expected to find, or explains where
// the file stopped matching — a silent miss would let drift through the gate.
function extract({ file, pattern, label }, group = 1) {
const found = read(file).match(pattern);
if (!found) {
throw new Error(`${file}: no ${label} version found (pattern drifted?)`);
}
return found[group];
}

function sourceVersion() {
const version = extract(SOURCE);
if (!SEMVER.test(version)) {
throw new Error(`${SOURCE.file}: "${version}" is not a semantic version`);
}
return version;
}

function check() {
const expected = sourceVersion();
const problems = [];

for (const mirror of [...MIRRORS, CHANGELOG]) {
const actual = extract(mirror, mirror === CHANGELOG ? 1 : 2);
const status = actual === expected ? "ok" : `MISMATCH (${actual})`;
console.log(` ${actual === expected ? "✓" : "✗"} ${mirror.file} — ${status}`);
if (actual !== expected) {
problems.push(`${mirror.file} declares ${actual}, expected ${expected}`);
}
}

for (const { file, pattern, hint } of guards()) {
if (pattern.test(read(file))) {
problems.push(`${file} hardcodes a version — ${hint}`);
}
}

if (problems.length > 0) {
console.error(`\nVersion drift (source of truth: ${expected}):`);
for (const problem of problems) console.error(` - ${problem}`);
console.error("\nRun `node scripts/version.mjs --set <version>` to resync.");
process.exit(1);
}
console.log(`\nAll manifests agree on ${expected}.`);
}

function set(next) {
if (!SEMVER.test(next)) {
throw new Error(`"${next}" is not a semantic version`);
}
const previous = sourceVersion();

writeFileSync(
abs(SOURCE.file),
read(SOURCE.file).replace(SOURCE.pattern, `version = "${next}"`),
);
for (const { file, pattern } of MIRRORS) {
writeFileSync(
abs(file),
read(file).replace(pattern, (_, before, __, after) => `${before}${next}${after}`),
);
}

console.log(`${previous} -> ${next}`);
console.log(` ${[SOURCE.file, ...MIRRORS.map((m) => m.file)].join("\n ")}`);
console.log(`\nAdd a \`## ${next}\` section to CHANGELOG.md to complete the bump.`);
}

const USAGE = `Keep one version across the repo, sourced from ${SOURCE.file}.

usage: node scripts/version.mjs <command>

--check verify every manifest agrees; exits 1 on drift (CI gate)
--current print the version the repo declares
--set <version> rewrite the source and every mirror
--help show this message

Mirrors rewritten by --set:
${MIRRORS.map((mirror) => `${mirror.file} (${mirror.label})`).join("\n ")}

${CHANGELOG.file} is checked but never written — add its \`## <version>\` section by hand.`;

const [command, argument] = process.argv.slice(2);
try {
if (command === "--check") check();
else if (command === "--current") console.log(sourceVersion());
else if (command === "--set" && argument) set(argument);
else if (command === "--help" || command === "-h") console.log(USAGE);
else {
console.error(USAGE);
process.exit(2);
}
} catch (error) {
console.error(`version: ${error.message}`);
process.exit(1);
}
3 changes: 0 additions & 3 deletions sdks/java/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ plugins {
id("com.vanniktech.maven.publish") version "0.37.0"
}

group = "org.byteveda"
version = "0.20.0"

java {
// Sources + javadoc jars are added by the maven-publish plugin below.
// Compile to Java 17 bytecode with whatever JDK (>= 17) runs Gradle, rather
Expand Down
3 changes: 0 additions & 3 deletions sdks/java/graalvm-smoke/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ plugins {
id("org.graalvm.buildtools.native") version "0.10.6"
}

group = "org.byteveda"
version = "0.20.0"

repositories {
mavenCentral()
}
Expand Down
5 changes: 5 additions & 0 deletions sdks/java/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Coordinates for every project in the build — Gradle applies these to the root
# project and each subproject. `version` mirrors the root Cargo.toml and is
# rewritten by scripts/version.mjs; do not hand-edit it.
group=org.byteveda
version=0.20.0
3 changes: 0 additions & 3 deletions sdks/java/processor/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ plugins {
id("com.vanniktech.maven.publish") version "0.37.0"
}

group = "org.byteveda"
version = "0.20.0"

mavenPublishing {
publishToMavenCentral()
signAllPublications()
Expand Down
4 changes: 0 additions & 4 deletions sdks/java/spring/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ plugins {
id("com.vanniktech.maven.publish") version "0.37.0"
}

group = "org.byteveda"

version = "0.20.0"

mavenPublishing {
publishToMavenCentral()
signAllPublications()
Expand Down
3 changes: 0 additions & 3 deletions sdks/java/test-support/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ plugins {
id("com.vanniktech.maven.publish") version "0.37.0"
}

group = "org.byteveda"
version = "0.20.0"

mavenPublishing {
publishToMavenCentral()
signAllPublications()
Expand Down
4 changes: 3 additions & 1 deletion sdks/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ build-backend = "maturin"

[project]
name = "taskito"
version = "0.20.0"
# maturin reads the version off the crate at `tool.maturin.manifest-path`, so
# the wheel can never drift from the Rust core it wraps.
dynamic = ["version"]
description = "Rust-powered task queue for Python. No broker required."
requires-python = ">=3.10"
license = { file = "LICENSE" }
Expand Down
2 changes: 2 additions & 0 deletions sdks/python/taskito/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,6 @@

__version__ = _get_version("taskito")
except PackageNotFoundError:
# Running from a source tree with no installed distribution. Kept in sync
# with the root Cargo.toml by scripts/version.mjs — do not hand-edit.
__version__ = "0.20.0"