From 14085a977e41b5ca6ebdc297bf250106ee661347 Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Tue, 10 Mar 2026 15:16:00 +0000 Subject: [PATCH] initial commit --- .github/workflows/release.yml | 85 +++++++++++++++++++++ CONTRIBUTING.md | 18 +++++ docs-site/docs/features/orchestrator.md | 2 +- docs-site/docs/reference/faq.md | 6 ++ orchestrator/package.json | 2 +- orchestrator/src/client/lib/version.test.ts | 15 ++++ orchestrator/src/client/lib/version.ts | 26 +++---- orchestrator/vite.config.ts | 31 +++++--- package-lock.json | 2 +- package.json | 1 + scripts/set-orchestrator-version.mjs | 40 ++++++++++ 11 files changed, 197 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 orchestrator/src/client/lib/version.test.ts create mode 100644 scripts/set-orchestrator-version.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..37a786c --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ccbc7b..bba3cc8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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: diff --git a/docs-site/docs/features/orchestrator.md b/docs-site/docs/features/orchestrator.md index 0580006..0f345f0 100644 --- a/docs-site/docs/features/orchestrator.md +++ b/docs-site/docs/features/orchestrator.md @@ -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 `, 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 diff --git a/docs-site/docs/reference/faq.md b/docs-site/docs/reference/faq.md index db31800..e4094cf 100644 --- a/docs-site/docs/reference/faq.md +++ b/docs-site/docs/reference/faq.md @@ -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. diff --git a/orchestrator/package.json b/orchestrator/package.json index 6c62a79..e7d666c 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -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", diff --git a/orchestrator/src/client/lib/version.test.ts b/orchestrator/src/client/lib/version.test.ts new file mode 100644 index 0000000..4458f7e --- /dev/null +++ b/orchestrator/src/client/lib/version.test.ts @@ -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"); + }); +}); diff --git a/orchestrator/src/client/lib/version.ts b/orchestrator/src/client/lib/version.ts index 97b7fcb..77ee5fb 100644 --- a/orchestrator/src/client/lib/version.ts +++ b/orchestrator/src/client/lib/version.ts @@ -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 { ) { 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, diff --git a/orchestrator/vite.config.ts b/orchestrator/vite.config.ts index 86e0ca8..02a76af 100644 --- a/orchestrator/vite.config.ts +++ b/orchestrator/vite.config.ts @@ -1,22 +1,31 @@ /// -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), }, }); diff --git a/package-lock.json b/package-lock.json index a9e8ab8..206728d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5286224..15549f5 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/scripts/set-orchestrator-version.mjs b/scripts/set-orchestrator-version.mjs new file mode 100644 index 0000000..07299f5 --- /dev/null +++ b/scripts/set-orchestrator-version.mjs @@ -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 "); + 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}`);