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. 4. Include screenshots or short clips for UI changes when helpful.
5. Mention any tradeoffs or follow-up work in the PR description. 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) ## Validation Before PR (CI-Parity Checks)
Run from the repository root: 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 ### Opening documentation from the sidebar
1. Open the sidebar menu. 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 ### 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. 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? ## Where should contributors edit docs?
Edit files under `docs-site/docs` for latest docs. Edit files under `docs-site/docs` for latest docs.

View File

@ -1,6 +1,6 @@
{ {
"name": "job-ops-orchestrator", "name": "job-ops-orchestrator",
"version": "1.0.0", "version": "0.1.29",
"type": "module", "type": "module",
"description": "Unified orchestrator for job application pipeline", "description": "Unified orchestrator for job application pipeline",
"main": "src/server/index.ts", "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. * Normalize the app version into the user-facing release format.
* - Clean semver tags (v0.1.12) v0.1.12
* - Dev builds (v0.1.12-8-gabc123) abc123-dev
*/ */
export function parseVersion(rawVersion: string): string { export function parseVersion(rawVersion: string): string {
// If it's a clean semver tag (v0.1.12), return as-is const normalized = rawVersion.trim();
if (/^v\d+\.\d+\.\d+$/.test(rawVersion)) { if (/^v\d+\.\d+\.\d+$/.test(normalized)) {
return rawVersion; return normalized;
} }
// If it's a dev build (v0.1.12-8-gabc123), extract commit hash and add -dev if (/^\d+\.\d+\.\d+$/.test(normalized)) {
const match = rawVersion.match(/-g([a-f0-9]+)$/); return `v${normalized}`;
if (match) {
return `${match[1].slice(0, 7)}-dev`;
} }
// Fallback: return shortened hash return normalized || "unknown";
return rawVersion.length > 7
? `${rawVersion.slice(0, 7)}-dev`
: `${rawVersion}-dev`;
} }
/** /**
@ -80,11 +73,10 @@ export async function checkForUpdate(): Promise<VersionCheckResult> {
) { ) {
throw new Error("Invalid response format"); 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 = const updateAvailable =
/^v\d+\.\d+\.\d+$/.test(currentRaw) && latestVersion !== currentRaw; currentVersion !== "unknown" && latestVersion !== currentVersion;
const result: VersionCheckResult = { const result: VersionCheckResult = {
currentVersion, currentVersion,

View File

@ -1,22 +1,31 @@
/// <reference types="vitest" /> /// <reference types="vitest" />
import { execSync } from "node:child_process"; import { readFileSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
let gitVersion: string; function readAppVersion(): string {
try { const packageJsonPath = new URL("./package.json", import.meta.url);
gitVersion = execSync("git describe --tags --always", { const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
stdio: ["ignore", "pipe", "ignore"], version?: unknown;
}) };
.toString()
.trim(); if (
} catch { typeof packageJson.version !== "string" ||
gitVersion = process.env.APP_VERSION ?? "unknown"; !/^\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 { declare global {
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var __APP_VERSION__: string; var __APP_VERSION__: string;
@ -66,6 +75,6 @@ export default defineConfig({
emptyOutDir: true, emptyOutDir: true,
}, },
define: { 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": { "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", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
"integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
"license": "ISC", "license": "ISC",

View File

@ -20,6 +20,7 @@
"check:docs": "npm --workspace docs-site run build", "check:docs": "npm --workspace docs-site run build",
"docs:serve": "npm --workspace docs-site run serve", "docs:serve": "npm --workspace docs-site run serve",
"docs:version": "npm --workspace docs-site run docs:version", "docs:version": "npm --workspace docs-site run docs:version",
"release:set-version": "node ./scripts/set-orchestrator-version.mjs",
"knip": "knip" "knip": "knip"
}, },
"devDependencies": { "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}`);