{"record":{"id":"1e4fdef6455257c2","repo":"santifer/career-ops","slug":"gem-cannot-derive-board-id-for-entry-name","errorCode":null,"errorMessage":"gem: cannot derive board id for ${entry.name}","messagePattern":"gem: cannot derive board id for (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/gem.mjs","lineNumber":153,"sourceCode":"function formatLocation(loc) {\n  const parts = [];\n  if (typeof loc?.name === 'string' && loc.name.trim()) parts.push(loc.name.trim());\n  if (loc?.isRemote) parts.push('Remote');\n  return parts.join(' · ');\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'gem',\n\n  detect(entry) {\n    const boardId = resolveBoardId(entry);\n    return boardId ? { url: `${GEM_API_URL}?board=${boardId}` } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const boardId = resolveBoardId(entry);\n    if (!boardId) throw new Error(`gem: cannot derive board id for ${entry.name}`);\n    assertGemUrl(GEM_API_URL);\n\n    const body = JSON.stringify([\n      { operationName: 'JobBoardList', variables: { boardId }, query: JOB_BOARD_LIST_QUERY },\n    ]);\n    // redirect:'error' prevents SSRF via server-side redirects; combined with\n    // assertGemUrl above it guarantees the final hostname stays in the allowlist.\n    const json = /** @type {any} */ (await ctx.fetchJson(GEM_API_URL, {\n      method: 'POST',\n      headers: { 'content-type': 'application/json', batch: 'true' },\n      body,\n      redirect: 'error',\n    }));\n\n    const listResult = json?.[0];\n    if (Array.isArray(listResult?.errors) && listResult.errors.length > 0) {\n      throw new Error(`gem: JobBoardList failed: ${listResult.errors[0]?.message || 'unknown GraphQL error'}`);\n    }","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/gem.mjs#L135-L171","documentation":"gem.mjs throws this at the top of fetch() when resolveBoardId(entry) returns null. resolveBoardId extracts the board id from entry.careers_url: it must be a parseable URL whose hostname is exactly jobs.gem.com and whose first path segment is the board id. If careers_url is missing, not a jobs.gem.com URL, or has no path segment, no board id can be derived and the provider refuses to fetch.","triggerScenarios":"A portals.yml entry has provider: gem but no careers_url; careers_url points at the company's own careers site instead of jobs.gem.com; careers_url is a jobs.gem.com board but written as a deep link (e.g. https://jobs.gem.com/board/123) where the segment is still captured but if the URL is malformed/empty it returns null; the hostname has a trailing slash variant or www prefix not equal to jobs.gem.com.","commonSituations":"Operator adds a Gem-hosted company but copies the careers page URL (e.g. https://company.com/careers) rather than the jobs.gem.com/<boardId> URL; the entry was auto-generated with a placeholder careers_url; the Gem board URL was typed with a typo in the host.","solutions":["Open the company's Gem board in a browser and copy the URL whose host is jobs.gem.com and whose first path segment is the board id (e.g. https://jobs.gem.com/acme).","Set careers_url to that URL in portals.yml for the provider: gem entry.","If you only know a non-Gem careers page, do not use provider: gem — pick the provider that actually backs that URL.","Verify there are no leading/trailing spaces or quotes around careers_url in the YAML."],"exampleFix":"# before\n- name: Acme\n  provider: gem\n  careers_url: https://acme.com/careers   # not a Gem board -> throws\n\n# after\n- name: Acme\n  provider: gem\n  careers_url: https://jobs.gem.com/acme","handlingStrategy":"validation","validationCode":"// Pre-flight check for a portals.yml entry before invoking the gem provider.\nfunction gemEntryIsValid(entry) {\n  if (entry.provider !== 'gem') return true;\n  const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';\n  if (!raw) return false;\n  let u;\n  try { u = new URL(raw); } catch { return false; }\n  if (u.hostname !== 'jobs.gem.com') return false;\n  return /^\\/[^/?#]+/.test(u.pathname); // has a board id segment\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always source the Gem board URL from the browser address bar of the live jobs.gem.com/<boardId> page.","Validate portals.yml entries with a lint pass that calls gemEntryIsValid for every provider: gem row.","Prefer detect() (which returns null on a bad entry) over fetch() when only probing."],"tags":["config","portals-yml","gem","provider-config","validation"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}