initial commit
This commit is contained in:
parent
ee6f889094
commit
14085a977e
85
.github/workflows/release.yml
vendored
Normal file
85
.github/workflows/release.yml
vendored
Normal 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
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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",
|
||||
|
||||
15
orchestrator/src/client/lib/version.test.ts
Normal file
15
orchestrator/src/client/lib/version.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
|
||||
@ -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
2
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
40
scripts/set-orchestrator-version.mjs
Normal file
40
scripts/set-orchestrator-version.mjs
Normal 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}`);
|
||||
Loading…
x
Reference in New Issue
Block a user