# Changelog of IT Hiring Signals: Tech Stack from Job Postings (`fecklesspleb/it-hiring-signals`) Actor

- **URL**: https://apify.com/fecklesspleb/it-hiring-signals/changelog.md
- **Full Actor documentation**: https://apify.com/fecklesspleb/it-hiring-signals.md

## Changelog

### Unreleased: blocked boards get their own count in the status row (1102x)

- When no board can be read, the free `allBoardsFailed` status row now counts boards refused by the private-address guard (error `not fetched: ...`) separately, as "blocked (private, loopback or link-local address)", the same label as career-site-jobs-feed. Before, a blocked careers page was counted as "not found (404 / no board)" and a blocked ATS board as "network or HTTP error".
- New field `boardsBlocked` on that row. `boardsNotFound` and `boardsNetworkError` no longer include blocked boards. No field is renamed; `failedBoards` keeps each board's `status` and error as before. Runs with no blocked board are unchanged, and nothing about prices or charging changes. Files: `CHANGELOG.md`, `README.md`, `my_actor/main.py`, `tests/test_blocked_status_row_1102x.py`.

### Unreleased: private-address guard (GAP17)

- On the Apify platform (`APIFY_IS_AT_HOME` set), every request now checks the host first: if any address it resolves to is private, loopback, link-local, reserved, multicast, unspecified or otherwise not global (also IPv4-mapped IPv6), the request is not sent. This covers careers-page URLs, every redirect hop (an httpx request hook on the client), robots.txt and the ATS API calls. Local runs are unchanged.
- A blocked careers page gets a free `unresolved` dataset row (and a RUN_SUMMARY entry) with `not fetched: private, loopback and link-local addresses are not allowed`. Nothing is charged for it. A site whose robots.txt redirects to a private address is skipped as "robots.txt unreadable". A host whose DNS lookup fails reads `DNS lookup failed for <host>` (same handling as before).
- No change to prices, events, input, output fields or `.actor/` files. Files: `CHANGELOG.md`, `README.md` (one FAQ entry), `my_actor/sources.py`, `my_actor/main.py`, `my_actor/engine.py`, `tests/test_ssrf_guard_1102t.py`.

### Unreleased: Console view shows status rows

- The "Tagged jobs" dataset view now also shows `recordType`, `status` ("Row status") and `message`, so free status rows and unresolved rows are readable in the Console.

### Unreleased: a run in which every board fails succeeds with a free status row

- Fixed: when no board could be read (network errors, 404s, robots.txt) the run failed and the dataset was empty (or held only the free `unresolved` rows). It now succeeds and saves one free status row: `recordType: "status"`, `statusRow: true`, `status: "error"`, `reason: "allBoardsFailed"`, `boardsFailed` / `boardsRequested`, `boardsNetworkError`, `boardsNotFound`, `boardsSkippedRobots`, `failedBoards` (first 50, with the error), and a `message` starting "Not charged: ...". The run status message is the same text.
- A run that reads boards but returns no job and no other row (filters, `onlyNew`, a maximum cost below one job) now saves one free `status: "warning"` row (`reason` `noJobs` or `costLimit`) instead of an empty dataset.
- `RUN_SUMMARY` has a new `statusRow` flag. Runs that return jobs are unchanged.

### 2026-10-29: empty-input demo capped at 4 jobs

- An empty input `{}` now stops the demo at 4 jobs (4 x $0.0015 = $0.006, under $0.01) instead of 100. Before, a cloud `{}` run with a $0.05 maximum cost stopped at the cost cap (33 jobs, $0.0495) instead of a job cap.
- The input prefill for `maxTotalJobs` is now 4 (was 100), so the prefilled Console form and the Store auto-test run the same 4-job demo. The `companies` prefill is unchanged. Raise `maxTotalJobs` for real runs; its default when omitted is still 1,000.
- New `exampleRunInput` in actor.json with the demo values (the 3 example boards, `maxTotalJobs` 4).
- An explicit `maxTotalJobs` other than the default 1,000 is still respected in demo mode.

### Unreleased: small run caps are shared round-robin between boards

- Fixed: with a small `maxTotalJobs` the first fast board filled the whole cap, so the Store demo and prefill (3 boards, 4 jobs) never showed Lever (run UK49Iwflmk2SbKAVd). When `maxTotalJobs` <= 100, 2+ boards are read and `maxJobsPerCompany` is not set, a board now takes its next job only when no other board still being read has fewer; a board that runs out frees its share for the others. New `capSharedRoundRobin` flag in RUN_SUMMARY. Demo/prefill cost is unchanged (4 jobs, at most $0.006). The vendored engine files are not edited (done in `ITRunner`).

### Unreleased: shared Greenhouse source reads workplaceType and employmentType from board custom fields

- The shared ATS source code now reads Greenhouse custom job-board fields (the API's `metadata`) such as `Workplace Type` or `Employment Type` into `workplaceType`/`employmentType` (normalized) and `workplaceTypeRaw`/`employmentTypeRaw`. Only Greenhouse jobs are affected; output from this Actor's own ATS is unchanged.

### 2026-10-29: empty input runs a small demo; Max directory companies accepts 0

- An empty input (the Apify Store auto-test) no longer fails. With no companies and no directory boards, the run reads 3 public example boards (`ashby:ashby`, `greenhouse:gitlab`, `lever:palantir`), at most 100 jobs (about $0.15), logs a warning, and RUN_SUMMARY has `demoMode: true`. An explicit `maxTotalJobs` other than the default 1000 is respected.
- `maxDirectoryCompanies: 0` now means "add no directory boards". Before, the input schema rejected 0 (minimum 1) and the code raised it to 1.

### 2026-10-29: one vocabulary for workplaceType and employmentType

- **Breaking for exact-value filters:** `workplaceType` is now always `remote`, `hybrid`, `onsite` or null, and `employmentType` is always one of the schema.org values `FULL_TIME`, `PART_TIME`, `CONTRACTOR`, `TEMPORARY`, `INTERN`, `VOLUNTEER`, `PER_DIEM`, `OTHER`, or null. Before, the ATS's own spelling was passed through (`on-site`, `OnSite`, `Full-time`, `FullTime`, `Full time`).
- The ATS's own text is kept in the new fields `workplaceTypeRaw` and `employmentTypeRaw`.
- A workplace value that says nothing clear (for example Lever's `unspecified`) becomes null; an employment value that fits none of the types (for example Lever's `Permanent`) becomes `OTHER`.

### 2026-10-29: no more placeholder Greenhouse requisition IDs

- `requisitionId` is now null when the company's field holds a placeholder (`ONE`, `MULTI`, `TBD`, `See Opening ID`) or a free-text note instead of an ID. Only a single code of up to 40 characters that contains a non-zero digit (`R-107348`, `REQ52463`) is kept, trimmed.

### 2026-10-29: robots.txt rule for every host

- If a host's robots.txt can't be read (a server error, a page that isn't a robots file, or a network failure), the whole host is now skipped for the run with a warning in the log. Before, such a host was read without rules.
- A server error or network failure on robots.txt is retried once, 1.5 seconds later, before the host is skipped. A missing robots.txt (any 4xx) still means no rules. A readable robots.txt is obeyed as before.
- The rule also covers detection probes (company-name and slug auto-detection, board lookups, careers-page sniffing). Redirects to another host are followed one hop at a time, and each host's robots.txt is checked first.
