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.
|
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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
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.
|
* 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,
|
||||||
|
|||||||
@ -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
2
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
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