Add startup.jobs extractor support (#279)

* Add startup.jobs extractor support

* Harden startup.jobs extractor inputs

* Wire startupjobs into Docker and CI

* Tighten startupjobs review follow-ups

* fix: publish ghcr during release workflow

* feat: add startupjobs max jobs configuration and update related tests
This commit is contained in:
Shaheer Sarfaraz
2026-03-17 12:20:45 +00:00
committed by GitHub
parent 26275e4ee8
commit 71e640b563
28 changed files with 1177 additions and 132 deletions
@@ -753,6 +753,7 @@ describe("OrchestratorPage", () => {
gradcrackerMaxJobsPerTerm: 150,
ukvisajobsMaxJobs: 150,
adzunaMaxJobsPerTerm: 150,
startupjobsMaxJobsPerTerm: 150,
jobspyCountryIndeed: "united kingdom",
searchCities: "United Kingdom",
});
@@ -182,6 +182,7 @@ export const AutomaticRunTab: React.FC<AutomaticRunTabProps> = ({
const rememberedRunBudget =
settings?.jobspyResultsWanted?.value ??
settings?.startupjobsMaxJobsPerTerm?.value ??
settings?.adzunaMaxJobsPerTerm?.value ??
settings?.gradcrackerMaxJobsPerTerm?.value ??
settings?.ukvisajobsMaxJobs?.value ??
@@ -52,6 +52,17 @@ describe("automatic-run utilities", () => {
expect(cap).toBeLessThanOrEqual(750);
});
it("assigns a dedicated startupjobs max-jobs limit", () => {
const limits = deriveExtractorLimits({
budget: 120,
searchTerms: ["backend", "platform"],
sources: ["startupjobs"],
});
expect(limits.startupjobsMaxJobsPerTerm).toBeGreaterThan(0);
expect(limits.startupjobsMaxJobsPerTerm).toBeLessThanOrEqual(120);
});
it("returns zero estimate when no search terms are provided", () => {
const estimate = calculateAutomaticEstimate({
values: {
@@ -112,4 +123,21 @@ describe("automatic-run utilities", () => {
expect(estimate.discovered.cap).toBeGreaterThan(0);
expect(estimate.discovered.cap).toBeLessThanOrEqual(120);
});
it("includes startupjobs in estimate caps using the shared term budget", () => {
const estimate = calculateAutomaticEstimate({
values: {
topN: 10,
minSuitabilityScore: 50,
searchTerms: ["backend", "platform"],
runBudget: 120,
country: "united kingdom",
cityLocations: [],
},
sources: ["startupjobs"],
});
expect(estimate.discovered.cap).toBeGreaterThan(0);
expect(estimate.discovered.cap).toBeLessThanOrEqual(120);
});
});
@@ -66,6 +66,7 @@ export interface ExtractorLimits {
gradcrackerMaxJobsPerTerm: number;
ukvisajobsMaxJobs: number;
adzunaMaxJobsPerTerm: number;
startupjobsMaxJobsPerTerm: number;
}
export function deriveExtractorLimits(args: {
@@ -82,6 +83,7 @@ export function deriveExtractorLimits(args: {
const includesUkVisaJobs = args.sources.includes("ukvisajobs");
const includesAdzuna = args.sources.includes("adzuna");
const includesHiringCafe = args.sources.includes("hiringcafe");
const includesStartupJobs = args.sources.includes("startupjobs");
const weightedContributors =
(includesIndeed ? termCount : 0) +
@@ -90,7 +92,8 @@ export function deriveExtractorLimits(args: {
(includesGradcracker ? termCount : 0) +
(includesUkVisaJobs ? 1 : 0) +
(includesAdzuna ? termCount : 0) +
(includesHiringCafe ? termCount : 0);
(includesHiringCafe ? termCount : 0) +
(includesStartupJobs ? termCount : 0);
if (weightedContributors <= 0) {
return {
@@ -98,6 +101,7 @@ export function deriveExtractorLimits(args: {
gradcrackerMaxJobsPerTerm: budget,
ukvisajobsMaxJobs: budget,
adzunaMaxJobsPerTerm: budget,
startupjobsMaxJobsPerTerm: budget,
};
}
@@ -109,6 +113,7 @@ export function deriveExtractorLimits(args: {
gradcrackerMaxJobsPerTerm: perUnit,
ukvisajobsMaxJobs: Math.min(budget, perUnit + remainder),
adzunaMaxJobsPerTerm: perUnit,
startupjobsMaxJobsPerTerm: perUnit,
};
}
@@ -173,6 +178,7 @@ export function calculateAutomaticEstimate(args: {
const hasGlassdoor = sources.includes("glassdoor");
const hasAdzuna = sources.includes("adzuna");
const hasHiringCafe = sources.includes("hiringcafe");
const hasStartupJobs = sources.includes("startupjobs");
const limits = deriveExtractorLimits({
budget: values.runBudget,
searchTerms: values.searchTerms,
@@ -191,9 +197,17 @@ export function calculateAutomaticEstimate(args: {
const hiringCafeCap = hasHiringCafe
? limits.jobspyResultsWanted * termCount
: 0;
const startupJobsCap = hasStartupJobs
? limits.startupjobsMaxJobsPerTerm * termCount
: 0;
const discoveredCap =
jobspyCap + gradcrackerCap + ukvisaCap + adzunaCap + hiringCafeCap;
jobspyCap +
gradcrackerCap +
ukvisaCap +
adzunaCap +
hiringCafeCap +
startupJobsCap;
const discoveredMin = Math.round(discoveredCap * 0.35);
const discoveredMax = Math.round(discoveredCap * 0.75);
const processedMin = Math.min(values.topN, discoveredMin);
@@ -181,11 +181,13 @@ export function usePipelineControls(
);
const hasAdzuna = compatibleSources.includes("adzuna");
const hasHiringCafe = compatibleSources.includes("hiringcafe");
const hasStartupJobs = compatibleSources.includes("startupjobs");
const serializedCities = serializeCityLocationsSetting(
values.cityLocations,
);
const searchCities =
(hasJobSpySite || hasAdzuna || hasHiringCafe) && serializedCities
(hasJobSpySite || hasAdzuna || hasHiringCafe || hasStartupJobs) &&
serializedCities
? serializedCities
: formatCountryLabel(values.country);
await api.updateSettings({
@@ -194,6 +196,7 @@ export function usePipelineControls(
gradcrackerMaxJobsPerTerm: limits.gradcrackerMaxJobsPerTerm,
ukvisajobsMaxJobs: limits.ukvisajobsMaxJobs,
adzunaMaxJobsPerTerm: limits.adzunaMaxJobsPerTerm,
startupjobsMaxJobsPerTerm: limits.startupjobsMaxJobsPerTerm,
jobspyCountryIndeed: values.country,
searchCities,
});
@@ -17,6 +17,10 @@ describe("orchestrator utils", () => {
expect(getEnabledSources(withoutKey)).not.toContain("adzuna");
});
it("enables startupjobs without credentials", () => {
expect(getEnabledSources(createAppSettings())).toContain("startupjobs");
});
it("counts processing jobs in ready and discovered tabs", () => {
const jobs = [
createJob({ id: "ready", status: "ready", closedAt: null }),
@@ -195,6 +195,10 @@ export const getEnabledSources = (
enabled.push(source);
continue;
}
if (source === "startupjobs") {
enabled.push(source);
continue;
}
if (
source === "indeed" ||
source === "linkedin" ||
@@ -254,6 +254,7 @@ export const DEMO_SOURCE_BASE_URLS: Record<JobSource, string> = {
ukvisajobs: "https://www.ukvisajobs.com",
adzuna: "https://www.adzuna.com",
hiringcafe: "https://hiring.cafe",
startupjobs: "https://startup.jobs",
manual: "https://example.com",
};