initial commit

This commit is contained in:
DaKheera47 2026-03-10 15:16:00 +00:00
parent ee6f889094
commit 14085a977e
11 changed files with 197 additions and 31 deletions

85
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,85 @@
name: release
on:
workflow_dispatch:
inputs:
version:
description: "Next release version (x.y.z)"
required: true
type: string
permissions:
contents: write
concurrency:
group: release-${{ inputs.version }}
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Validate release version
env:
RELEASE_VERSION: ${{ inputs.version }}
run: |
if ! [[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Version must be in x.y.z form, got '$RELEASE_VERSION'" >&2
exit 1
fi
CURRENT_VERSION="$(node -p "require('./orchestrator/package.json').version")"
if [ "$CURRENT_VERSION" = "$RELEASE_VERSION" ]; then
echo "Version $RELEASE_VERSION is already set in orchestrator/package.json" >&2
exit 1
fi
if git rev-parse "v$RELEASE_VERSION" >/dev/null 2>&1; then
echo "Tag v$RELEASE_VERSION already exists locally" >&2
exit 1
fi
if git ls-remote --tags origin "refs/tags/v$RELEASE_VERSION" | grep -q .; then
echo "Tag v$RELEASE_VERSION already exists on origin" >&2
exit 1
fi
- name: Bump orchestrator version files
env:
RELEASE_VERSION: ${{ inputs.version }}
run: node ./scripts/set-orchestrator-version.mjs "$RELEASE_VERSION"
- name: Commit and push version bump
env:
RELEASE_VERSION: ${{ inputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add orchestrator/package.json package-lock.json
git commit -m "chore: release $RELEASE_VERSION"
git push origin HEAD:main
- name: Create and push release tag
env:
RELEASE_VERSION: ${{ inputs.version }}
run: |
git tag "v$RELEASE_VERSION"
git push origin "v$RELEASE_VERSION"
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ inputs.version }}
run: gh release create "v$RELEASE_VERSION" --title "v$RELEASE_VERSION" --generate-notes

View File

@ -54,6 +54,24 @@ Local URLs:
4. Include screenshots or short clips for UI changes when helpful.
5. Mention any tradeoffs or follow-up work in the PR description.
## Releases
Releases are driven from GitHub Actions.
1. Open the `release` workflow in GitHub Actions.
2. Enter the next version as `x.y.z` (for example `0.1.30`).
3. Run the workflow.
The workflow will:
- bump `orchestrator/package.json`
- update `package-lock.json`
- commit the version bump to `main`
- create and push tag `vX.Y.Z`
- create the GitHub release
The app version shown in the UI is sourced from `orchestrator/package.json`, so the release version, tag, and displayed app version stay aligned.
## Validation Before PR (CI-Parity Checks)
Run from the repository root:

View File

@ -73,7 +73,7 @@ Open the **search links** row in the Ready summary to reveal the generated links
### Opening documentation from the sidebar
1. Open the sidebar menu.
2. In the footer section under `Version <build>`, click **Documentation**, which opens the locally hosted docs in a new tab.
2. In the footer section under `Version vX.Y.Z`, click **Documentation**, which opens the locally hosted docs in a new tab.
### Generating PDFs

View File

@ -13,6 +13,12 @@ Yes. The docs static build is bundled and served locally at `/docs`.
Docs are versioned using Docusaurus versions, intended to map to release tags.
## How is the app version managed?
The app version comes from `orchestrator/package.json`.
Releases create a matching `vX.Y.Z` Git tag, and the UI shows that release version in the sidebar footer.
## Where should contributors edit docs?
Edit files under `docs-site/docs` for latest docs.

View File

@ -1,6 +1,6 @@
{
"name": "job-ops-orchestrator",
"version": "1.0.0",
"version": "0.1.29",
"type": "module",
"description": "Unified orchestrator for job application pipeline",
"main": "src/server/index.ts",

View File

@ -0,0 +1,15 @@
import { parseVersion } from "./version";
describe("version", () => {
it("normalizes bare semver into a release tag", () => {
expect(parseVersion("0.1.30")).toBe("v0.1.30");
});
it("keeps prefixed release tags unchanged", () => {
expect(parseVersion("v0.1.30")).toBe("v0.1.30");
});
it("falls back to unknown for empty values", () => {
expect(parseVersion("")).toBe("unknown");
});
});

View File

@ -20,24 +20,17 @@ export interface VersionCheckResult {
}
/**
* Parse git version string into display format.
* - Clean semver tags (v0.1.12) v0.1.12
* - Dev builds (v0.1.12-8-gabc123) abc123-dev
* Normalize the app version into the user-facing release format.
*/
export function parseVersion(rawVersion: string): string {
// If it's a clean semver tag (v0.1.12), return as-is
if (/^v\d+\.\d+\.\d+$/.test(rawVersion)) {
return rawVersion;
const normalized = rawVersion.trim();
if (/^v\d+\.\d+\.\d+$/.test(normalized)) {
return normalized;
}
// If it's a dev build (v0.1.12-8-gabc123), extract commit hash and add -dev
const match = rawVersion.match(/-g([a-f0-9]+)$/);
if (match) {
return `${match[1].slice(0, 7)}-dev`;
if (/^\d+\.\d+\.\d+$/.test(normalized)) {
return `v${normalized}`;
}
// Fallback: return shortened hash
return rawVersion.length > 7
? `${rawVersion.slice(0, 7)}-dev`
: `${rawVersion}-dev`;
return normalized || "unknown";
}
/**
@ -80,11 +73,10 @@ export async function checkForUpdate(): Promise<VersionCheckResult> {
) {
throw new Error("Invalid response format");
}
const latestVersion = (data as { tag_name: string }).tag_name;
const latestVersion = parseVersion((data as { tag_name: string }).tag_name);
// Update available if current is a clean tag and differs from latest
const updateAvailable =
/^v\d+\.\d+\.\d+$/.test(currentRaw) && latestVersion !== currentRaw;
currentVersion !== "unknown" && latestVersion !== currentVersion;
const result: VersionCheckResult = {
currentVersion,

View File

@ -1,22 +1,31 @@
/// <reference types="vitest" />
import { execSync } from "node:child_process";
import { readFileSync } from "node:fs";
import path from "node:path";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
let gitVersion: string;
try {
gitVersion = execSync("git describe --tags --always", {
stdio: ["ignore", "pipe", "ignore"],
})
.toString()
.trim();
} catch {
gitVersion = process.env.APP_VERSION ?? "unknown";
function readAppVersion(): string {
const packageJsonPath = new URL("./package.json", import.meta.url);
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
version?: unknown;
};
if (
typeof packageJson.version !== "string" ||
!/^\d+\.\d+\.\d+$/.test(packageJson.version)
) {
throw new Error(
"orchestrator/package.json must contain a semver version in x.y.z format",
);
}
return `v${packageJson.version}`;
}
const appVersion = readAppVersion();
declare global {
// eslint-disable-next-line no-var
var __APP_VERSION__: string;
@ -66,6 +75,6 @@ export default defineConfig({
emptyOutDir: true,
},
define: {
__APP_VERSION__: JSON.stringify(gitVersion),
__APP_VERSION__: JSON.stringify(appVersion),
},
});

2
package-lock.json generated
View File

@ -2645,7 +2645,7 @@
}
},
"node_modules/@crawlee/templates/node_modules/mute-stream": {
"version": "1.0.0",
"version": "0.1.29",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
"integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
"license": "ISC",

View File

@ -20,6 +20,7 @@
"check:docs": "npm --workspace docs-site run build",
"docs:serve": "npm --workspace docs-site run serve",
"docs:version": "npm --workspace docs-site run docs:version",
"release:set-version": "node ./scripts/set-orchestrator-version.mjs",
"knip": "knip"
},
"devDependencies": {

View File

@ -0,0 +1,40 @@
import { readFileSync, writeFileSync } from "node:fs";
const nextVersion = process.argv[2]?.trim();
if (!nextVersion || !/^\d+\.\d+\.\d+$/.test(nextVersion)) {
console.error("Usage: node ./scripts/set-orchestrator-version.mjs <x.y.z>");
process.exit(1);
}
const orchestratorPackagePath = new URL(
"../orchestrator/package.json",
import.meta.url,
);
const packageLockPath = new URL("../package-lock.json", import.meta.url);
const orchestratorPackage = JSON.parse(
readFileSync(orchestratorPackagePath, "utf8"),
);
if (orchestratorPackage.version === nextVersion) {
console.log(`orchestrator/package.json already at ${nextVersion}`);
} else {
orchestratorPackage.version = nextVersion;
writeFileSync(
orchestratorPackagePath,
`${JSON.stringify(orchestratorPackage, null, 2)}\n`,
);
console.log(`Updated orchestrator/package.json to ${nextVersion}`);
}
const packageLock = JSON.parse(readFileSync(packageLockPath, "utf8"));
if (!packageLock.packages?.orchestrator) {
console.error("package-lock.json is missing packages.orchestrator");
process.exit(1);
}
packageLock.packages.orchestrator.version = nextVersion;
writeFileSync(packageLockPath, `${JSON.stringify(packageLock, null, 2)}\n`);
console.log(`Updated package-lock.json orchestrator entry to ${nextVersion}`);