Skip to content

feat(source-greenhouse): non-breaking prep for Harvest v3 migration - #83811

Merged
Patrick Nilan (pnilan) merged 10 commits into
masterfrom
devin/1786404163-greenhouse-v3-phase1
Aug 12, 2026
Merged

feat(source-greenhouse): non-breaking prep for Harvest v3 migration#83811
Patrick Nilan (pnilan) merged 10 commits into
masterfrom
devin/1786404163-greenhouse-v3-phase1

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

What

Phase 1 of the Greenhouse Harvest v3 migration (Harvest v1/v2 is removed by Greenhouse on 2026-08-31). This lands everything that can be done while still calling Harvest v1, so the breaking Phase 2 PR shrinks to schema + auth only.

Resolves https://github.com/airbytehq/airbyte-internal-issues/issues/16900:

Parent epic: airbytehq/oncall#10401. Phase 2: airbytehq/airbyte-internal-issues#16901.

Not a breaking change (evaluated against the breaking-change checklist): no field added/removed/renamed/retyped, no PK or cursor-field change, no spec field added or removed, no stream removed, no state-format change, and no change to which records a stream returns. Endpoints, filter values and record output are unchanged. Released as MINOR, 0.7.330.8.0; no breakingChanges entry. Autopilot progressive rollout is enabled on this connector as part of this PR (per review request); autopilot rollouts do not require an -rc.x version suffix, so the version stays plain 0.8.0.

How

  1. Per-stream fully-qualified url — dropped base_requester.url_base and every stream path, replaced by one url per stream (url_base/path are deprecated in declarative_component_schema.yaml in favour of url). Same v1 endpoints, Jinja path segments preserved verbatim. Phase 2's endpoint repoint becomes one line per stream.

  2. Cursor-safe page size — Harvest v3 rejects any query parameter sent alongside cursor (422), but page_size_option is injected on every request, including next-page requests (the paginator's page_token_option: RequestPath already supplies a complete next-page URL). page_size_option was removed and re-expressed as a conditional request parameter:

    request_parameters:
      per_page: "{{ 100 if not next_page_token }}"

    Against v1 this is behaviourally identical: v1's next-page Link URL already carries per_page (documented, and confirmed by a captured header), so the value on the wire is unchanged. per_page stays at 100 here; Phase 2 raises it to v3's max of 500. The 6 streams that have no paginator today (activity_feed, approvals, disciplines, schools, tags, user_permissions) deliberately did not gain a per_page.

    The incremental cursor filter is deliberately not gated. An earlier revision of this PR also made created_after/submitted_after/updated_after first-page-only, as the issue asks. That was reverted on review: Greenhouse never documents which query parameters are carried into v1's rel="next" URL, and under v1's limit/offset pagination a page-2 request that lost its date filter would offset into the unfiltered collection and silently emit unrelated older records. The filter therefore stays on the typed start_time_option and is sent on every page, exactly as 0.7.33 does. Gating it belongs in Phase 2, where v3 bakes the filters into the opaque cursor and actually forbids them alongside it — see the documentation verification and Patrick Nilan (@pnilan)'s call. Net effect: Phase 1 pre-validates the page-size gate only.

    Note on the else branch. The issue proposed {{ 100 if not next_page_token else None }}. That form does not work: JinjaInterpolation evaluates the rendered "None", sees it is not a str/list (ValidRequestTypes), and falls back to the raw rendered string, so the request emits the literal per_page=None. Verified against airbyte-cdk==6.56.7:

    expression no token with token
    {{ 100 if not next_page_token else None }} {'per_page': '100'} {'per_page': 'None'}
    {{ 100 if not next_page_token }} {'per_page': '100'} {}
    {{ 100 if not next_page_token else '' }} {'per_page': '100'} {}

    The no-else form is used. (This also corrects the issue's claim that source-greenhouse-harvest-v3 contribution from NumberPiOso #82701's else '' gating leaves a surviving empty parameter — it is stripped by the same code path.)

  3. num_workers description reworded so it no longer hardcodes v1's "50 requests per 10 seconds". api_budget and default_concurrency are deliberately unchanged — the connector still calls v1, so the v1 ceiling still applies. The budget retune moves with the endpoint change in Phase 2.

  4. allowedHosts gains auth.greenhouse.io (the v3 client-credentials token endpoint would otherwise be blocked on Cloud).

  5. Documentation URLs repointed to harvestdocs.greenhouse.io: externalDocumentationUrls in metadata.yaml, and the per-stream reference links in docs/integrations/sources/greenhouse.md. Every new URL was checked for HTTP 200. Four streams have no v3 reference page and keep their existing v1 links: Activity Feed, Degrees, Disciplines, Schools. The two Harvest API-key setup links (greenhouse.md lines 7 and 15) are intentionally left on v1 instructions — Phase 1 users still authenticate with a Harvest API key.

Declarative-First Evaluation

Declarative only — no custom Python component was added or modified. The conditional request_parameters interpolation on HttpRequester, plus the url field and the existing typed RequestOption, deliver the whole change; no new behaviour needed Python.

Decision on the overlapping PR

This supersedes #83323 (draft, adds two Harvest v3 externalDocumentationUrls entries alongside the v1 ones). This PR repoints the same metadata block wholesale, so #83323 should be closed rather than merged; a comment saying so has been left there. #82701 (community draft adding a separate source-greenhouse-harvest-v3 connector) is untouched — this epic is an in-place migration, so the two are not merged together.

Review guide

  1. airbyte-integrations/connectors/source-greenhouse/manifest.yaml — the bulk; mechanical and repetitive across 36 streams.
  2. airbyte-integrations/connectors/source-greenhouse/metadata.yaml
  3. docs/integrations/sources/greenhouse.md

Test Coverage

No unit test ships with this PR. One was originally added (a two-page mock-HTTP test asserting the exact page-1 and page-2 requests for applications and the parent-partitioned substream applications_interviews), along with a CDK pin bump in unit_tests/pyproject.toml/poetry.lock to match the base image. Both were removed at the maintainer's request — see the review thread on unit_tests/test_pagination_request_params.py — so the connector keeps having no unit-test suite of its own and the lockfile diff is gone. The evidence the test provided is covered by the prove-fix run below, which captured the same two-page request shapes deterministically against both this branch and 0.7.33.

Live regression run: done, and green. This session had no Greenhouse credentials, so the "no record diff vs the current release" acceptance criterion was initially left unverified. It has since been satisfied by a /ai-prove-fix run against this head (3d9e70dd02e, pre-release 0.8.0-preview.3d9e70d) — see the prove-fix comment on this PR for the full evidence. Summary: comparison vs. 0.7.33 over 2,192 records on three streams that each page past 100 records (jobs 830, offers 779, users 583) showed zero record delta, identical PKs and field values, unchanged catalog schema and state shape, and the only spec change being the intended num_workers.description reword. A separate deterministic two-page capture confirms page 2 carries the cursor filter and no per_page on this branch while 0.7.33 still sends per_page there.

User Impact

None expected. Same endpoints, same records, same state; the only wire difference is that page-2+ requests no longer repeat per_page, which v1's own next-page URL already carries. Users see only updated documentation links and a reworded num_workers description.

Can this PR be safely reverted and rolled back?

  • YES 💚
  • NO ❌

Link to Devin session: https://app.devin.ai/sessions/cb3111aa07fc44aea1658f96fab6c5cb

Important

Auto-merge enabled.

This PR is set to merge automatically when all requirements are met.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

Copy link
Copy Markdown
Contributor

👋 Greetings, Airbyte Team Member!

Here are some helpful tips and reminders for your convenience.

💡 Show Tips and Tricks

PR Slash Commands

Airbyte Maintainers (that's you!) can execute the following slash commands on your PR:

  • 🛠️ Quick Fixes
    • /format-fix - Fixes most formatting issues.
    • /bump-version - Bumps connector versions, scraping changelog description from the PR title.
      • Bump types: patch (default), minor, major, major_rc, rc, promote.
      • The rc type is a smart default: applies minor_rc if stable, or bumps the RC number if already RC.
      • The promote type strips the RC suffix to finalize a release.
      • Example: /bump-version type=rc or /bump-version type=minor
    • /bump-progressive-rollout-version - Alias for /bump-version type=rc. Bumps with an RC suffix and enables progressive rollout.
  • ❇️ AI Testing and Review (internal link: AI-SDLC Docs):
    • /ai-prove-fix - Runs prerelease readiness checks, including testing against customer connections.
    • /ai-canary-prerelease - Rolls out prerelease to 5-10 connections for canary testing.
    • /ai-review - AI-powered PR review for connector safety and quality gates.
  • 📝 AI Documentation:
    • /ai-docs-review - AI-powered documentation review for PRs with connector changes.
    • /ai-create-docs-pr - Creates a documentation PR for connector changes, stacked on the current PR.
  • 🚀 Connector Releases:
    • /publish-connectors-prerelease - Publishes pre-release connector builds (tagged as {version}-preview.{git-sha}) for all modified connectors in the PR.
    • /enable-autopilot-rollouts - Enables autopilot progressive rollouts for the modified connector(s) in the PR, remediating "autopilot rollouts not enabled for {connector-name}" auto-merge blockers. Sets defaultRolloutMode: autopilot and enableProgressiveRollout: true, preserving any existing autopilotConfig.
      • Optional args: connector=<CONNECTOR_NAME> (defaults to the modified connectors in the PR), strategy=fast|slow|default (defaults to fast).
      • Example: /enable-autopilot-rollouts or /enable-autopilot-rollouts connector=source-faker strategy=slow
  • ☕️ JVM connectors:
    • /update-connector-cdk-version connector=<CONNECTOR_NAME> - Updates the specified connector to the latest CDK version.
      Example: /update-connector-cdk-version connector=destination-bigquery
  • 🐍 Python connectors:
    • /poe connector source-example lock - Run the Poe lock task on the source-example connector, committing the results back to the branch.
    • /poe source example lock - Alias for /poe connector source-example lock.
    • /poe source example use-cdk-branch my/branch - Pin the source-example CDK reference to the branch name specified.
    • /poe source example use-cdk-latest - Update the source-example CDK dependency to the latest available version.
  • ⚙️ Admin commands:
    • /force-merge reason="<REASON>" - Force merges the PR using admin privileges, bypassing CI checks. Requires a reason.
      Example: /force-merge reason="CI is flaky, tests pass locally"
📚 Show Repo Guidance

Helpful Resources

📝 Edit this welcome message.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Note

✅ All modified connector(s) now have autopilot progressive rollouts enabled. Thanks!

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

source-greenhouse Connector Test Results

6 tests   4 ✅  12s ⏱️
2 suites  2 💤
2 files    0 ❌

Results for commit a7479c7.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Deploy preview for airbyte-docs ready!

Project:airbyte-docs
Status: ✅  Deploy successful!
Preview URL:https://airbyte-docs-ga37cn2py-airbyte-growth.vercel.app
Latest Commit:a7479c7

Deployed with vercel-action

Co-Authored-By: bot_apk <apk@cognition.ai>
@airbyte-support-bot

Copy link
Copy Markdown
Contributor

↪️ Triggering /ai-resolve-conflicts per Hands-Free AI Triage Project triage next step. Reason: Newly opened fix PR is conflicted with master (DIRTY); conflict resolution is required before /ai-review can be run.

@octavia-bot

octavia-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🔀 AI Resolve Conflicts session starting... Merging the latest master into the PR branch and resolving conflicts, then pushing the resolved branch so its CI and review gates re-run on the new HEAD. View playbook

Devin AI session created successfully!

Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🔀 Resolved merge conflict with master.

Resolution: master's dependency-update release (0.7.33, #83956) collided with this branch in metadata.yaml and the greenhouse.md changelog. Kept this branch's higher dockerImageTag: 0.8.0 (master's newer baseImage 7.24.0 merged in cleanly) and kept both changelog entries, ordered by version.

Pushed the resolved branch; its CI and review gates will re-run on the new HEAD.


Devin session

devin-ai-integration Bot and others added 2 commits August 11, 2026 11:45
Co-Authored-By: bot_apk <apk@cognition.ai>
…hase1' into devin/1786404163-greenhouse-v3-phase1
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

☑️ Conflict with master resolved in 4fe88e9 (no /ai-resolve-conflicts run needed).

The conflict came from #83956 (deps(source-greenhouse)), which bumped dockerImageTag to 0.7.33 and the base image to SDM 7.24.0. Resolution: kept our 0.8.0 (it supersedes 0.7.33), took master's new base image, and kept both changelog rows (0.8.0 above 0.7.33).

Since the base image moved, I also re-constructed the manifest against the CDK bundled in SDM 7.24.0: all 36 streams build, the fully-qualified url field and the base_requester without url_base are accepted, and first/second-page request params are identical to those produced under CDK 6.56.7. Unit tests pass locally.


Devin session

@pnilan Patrick Nilan (pnilan) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #83811 - API-Source Authoritative Review Findings

🤖 This comment was generated by an AI Agent.

Reduced-confidence review

This review ran in a degraded state. Every degradation recorded for this run, verbatim:

  • no Codex reviewer returned a result: this run had NO independent second-model generation pass. Detail: completeness: NO RESULT (No result.json ever written; codex-panel-completeness-raw.txt is 0 bytes. codex-panel-completeness-result.json.detach.log (802KB) shows a terminal wrapper failure, not an in-progress run: first line 'Structured output error: Codex exited with status 1: OpenAI Codex v0.146.0', and the log ends with 'ERROR: Codex ran out of room in the model's context window. Start a new thread or clear earlier history before retrying.' (printed twice) followed by 'tokens used' / '0'. No live process matches 'codex-panel-.*-prompt' (checked via pgrep/ps and a bounded poll loop that returned NO_LIVE_RUNS immediately) - the run is dead, not still executing. Root cause: the ~801KB prompt file overflowed the model's context window before any tokens were processed.); apidoc: NO RESULT (No result.json ever written; codex-panel-apidoc-raw.txt is 0 bytes. codex-panel-apidoc-result.json.detach.log (819KB) shows the same terminal wrapper failure: first line 'Structured output error: Codex exited with status 1: OpenAI Codex v0.146.0', log ends with 'ERROR: Codex ran out of room in the model's context window. Start a new thread or clear earlier history before retrying.' (x2) then 'tokens used' / '0'. No live process found (pgrep/ps + poll loop returned NO_LIVE_RUNS). Root cause: ~818KB prompt overflowed the context window pre-processing.); cdk: NO RESULT (No result.json ever written; codex-panel-cdk-raw.txt is 0 bytes. codex-panel-cdk-result.json.detach.log (819KB) shows the same terminal wrapper failure: first line 'Structured output error: Codex exited with status 1: OpenAI Codex v0.146.0', log ends with 'ERROR: Codex ran out of room in the model's context window. Start a new thread or clear earlier history before retrying.' (x2) then 'tokens used' / '0'. No live process found (pgrep/ps + poll loop returned NO_LIVE_RUNS). Root cause: ~819KB prompt overflowed the context window pre-processing.); schema: NO RESULT (No result.json ever written; codex-panel-schema-raw.txt is 0 bytes. codex-panel-schema-result.json.detach.log (819KB) shows the same terminal wrapper failure: first line 'Structured output error: Codex exited with status 1: OpenAI Codex v0.146.0', log ends with 'ERROR: Codex ran out of room in the model's context window. Start a new thread or clear earlier history before retrying.' (x2) then 'tokens used' / '0'. No live process found (pgrep/ps + poll loop returned NO_LIVE_RUNS). Root cause: ~819KB prompt overflowed the context window pre-processing.); incremental: NO RESULT (No result.json ever written; codex-panel-incremental-raw.txt is 0 bytes. codex-panel-incremental-result.json.detach.log (819KB) shows the same terminal wrapper failure: first line 'Structured output error: Codex exited with status 1: OpenAI Codex v0.146.0', log ends with 'ERROR: Codex ran out of room in the model's context window. Start a new thread or clear earlier history before retrying.' (x2) then 'tokens used' / '0'. No live process found (pgrep/ps + poll loop returned NO_LIVE_RUNS). Root cause: ~818KB prompt overflowed the context window pre-processing.); testing: NO RESULT (No result.json ever written; codex-panel-testing-raw.txt is 0 bytes. codex-panel-testing-result.json.detach.log (799KB) shows the same terminal wrapper failure: first line 'Structured output error: Codex exited with status 1: OpenAI Codex v0.146.0', log ends with 'ERROR: Codex ran out of room in the model's context window. Start a new thread or clear earlier history before retrying.' (x2) then 'tokens used' / '0'. No live process found (pgrep/ps + poll loop returned NO_LIVE_RUNS). Root cause: ~798KB prompt overflowed the context window pre-processing.); breaking: NO RESULT (No result.json ever written; codex-panel-breaking-raw.txt is 0 bytes. codex-panel-breaking-result.json.detach.log (820KB) shows the same terminal wrapper failure: first line 'Structured output error: Codex exited with status 1: OpenAI Codex v0.146.0', log ends with 'ERROR: Codex ran out of room in the model's context window. Start a new thread or clear earlier history before retrying.' (x2) then 'tokens used' / '0'. No live process found (pgrep/ps + poll loop returned NO_LIVE_RUNS). Root cause: ~820KB prompt overflowed the context window pre-processing.)

Because of this, do not read agreement between two independent generating models into any finding below, and do not treat the absence of a finding as evidence that a dimension is clean: for all seven dimensions (completeness, api-docs, cdk-patterns, schema, incremental, testing, breaking-change) the second-model generation pass produced nothing at all rather than producing a clean bill of health. The Claude-side generation pass and both validation passes did complete, so the findings that are present were independently validated; coverage breadth, not finding quality, is what is reduced.

Review metadata

Field Value
PR #83811
Connector source-greenhouse (low-code-components)
Head 4fe88e9370b3f8f1ad451047ce6d720dbf0a4b85
CDK (authoritative reference) pinned 7.24.0 - the version in the connector's base image
CDK main (upgrade reference only) 68bc2941768ce02b987d727e8113b14d90204684
Date 2026-08-11
Provenance Reviewed across 7 API-source dimensions by 7/7 Claude reviewers and 0/7 Codex (gpt-5.5) reviewers. CDK ground truth: the connector pinned version 7.24.0 (worktree verified); origin/main @ 68bc2941768ce02b987d727e8113b14d90204684 consulted only as an upgrade reference Third-party API docs: fetched. GitHub CI: 35 passed, 0 failed, 0 pending, 14 skipped; connector CI ran. Findings were mechanically anchored to changed lines, merged, then validated (17/17 Claude verdicts, 17/17 Codex verdicts) and reconciled. Review status: DEGRADED. Claude-side agents ran on the session model; no model version is asserted here.

Checks

GitHub CI: 35 passed / 0 failed / 0 pending / 14 not-passed (13 skipped + 1 cancelled). No failing checks, and none pending. Connector CI did run (same-repo branch, not a gated fork): Test source-greenhouse Connector passed - 3 unit tests under the lockfile CDK 6.56.7 plus the full acceptance suite against a real built image with live Greenhouse credentials (11 passed, 3 skipped).

Skipped and cancelled checks are not passes. The ones that matter here:

Check Status Note
Build and Verify Artifacts SKIPPED image build/spec/check/read was nevertheless exercised inside 'Test source-greenhouse Connector'
Pre-Release Checks SKIPPED no pre-release image was published, so no live regression / record-diff run happened
Connectors CDK Version Check SKIPPED the unit_tests CDK pin bump 6.10.0 -> 6.56.7 was never evaluated by the CDK-version gate
Validate PR Title SKIPPED bot author; 'Enforce PR structure' did pass
CodeQL (umbrella) NEUTRAL/skipping 'Analyze Python' and 'Analyze (python)' both passed
Call Connector CI Tests SKIPPED fork-gated workflow; not a gap here - same-repo branch, and connectors_ci ran the connector tests directly
Vercel Preview CANCELLED superseded by 'Vercel - airbyte-docs', which passed

PR-specific experiments were run to produce evidence CI cannot:

Kind Experiment Status What it established
regression_control Revert whole fix: run new test against pre-PR manifest passed Both new parametrized cases FAIL against the pre-PR manifest (NoMockAddress on a second-page URL carrying per_page=100 and created_after), so the new test genuinely gates the fix and fails for the exact reason the PR body claims.
regression_control Surgical revert: un-gate only the request params, keep the url refactor passed With the url_base->url refactor left intact and only the 'if not next_page_token' guard removed, both cases still fail - isolating the failure to request-parameter gating rather than the URL restructuring.
coverage_completeness Enumerate all streams in the $ref-resolved manifest passed 36/36 streams carry a fully-qualified url with no residual path/url_base; 30/30 paginated streams carry gated per_page and the 6 unpaginated streams carry none; 15/15 incremental streams carry exactly one gated cursor filter; zero page_size_option/start_time_option survive. 0 problems.
behaviour_probe Pre/post endpoint equivalence across all 36 streams passed Resolved old url_base+path vs new url for every stream: 36/36 identical, 0 streams added or removed, no diff in http_method / error_handler / authenticator. Substantiates the 'same v1 endpoints' claim mechanically rather than by reading the diff.
version_matrix Unit suite under base-image CDK 7.24.0 vs lockfile CDK 6.56.7 FAILED 3 passed under the lockfile pin 6.56.7 (what CI ran). Under 7.24.0 - the version in the connector's own base image - the two NEW tests error out: AttributeError 'DefaultStream' object has no attribute 'stream_slices' / 'retriever'. Test-harness incompatibility, not a manifest defect; this is the measured basis for F02 and F07.
behaviour_probe Request-parameter interpolation under both CDK versions passed Byte-identical results on 6.56.7 and 7.24.0: the gated form yields page1 {per_page, cursor filter} and page2 {}; the un-gated form still carries both on page 2; the issue's 'else None' form yields per_page='None'. The manifest change is version-stable; only the test is CDK-6-bound.
behaviour_probe Live request shapes emitted by the CI acceptance run (SDM 7.24.0 image) passed 27 distinct live first-page URLs recovered from CI's own acceptance log: every paginated stream emitted per_page=100 (plus the cursor filter where incremental), while disciplines, schools and tags/candidate emitted no query string - matching the static table. No second-page cursor URL appears anywhere in the log, so live evidence covers first-page shape only.
fallback_suite Run connector unit tests as CI ground-truth substitute not run Not applicable - CI was available and connector CI ran (3 unit tests, plus the full acceptance suite with real secrets), so there was no ground truth to recover.

Coverage caveat. CI built the connector image and ran spec/check/discover/read against live Greenhouse Harvest v1 with real credentials (11 passed, 3 skipped) plus 3 unit tests under the lockfile CDK 6.56.7, but no live regression/record-diff against the current release was performed (Pre-Release Checks skipped, no pre-release image), and the new mocked request-shape test cannot execute at all under CDK 7.24.0 - the version in the connector's own base image - so its assertions only cover the CDK-6 code path.

Summary

Severity Authoritative findings
🔴 P0 0
🟠 P1 0
🟡 P2 3
🔵 P3 5
⚪ P4 3
Total 11

Verdict: Fix before merge. No authoritative P0 and no unversioned breaking change, but three P2 findings should be resolved before merge.

6 candidate findings were dropped during validation (0 unvalidated); their ids, both reviewer verdicts and the reasoning are in pr-83811-api-source-authoritative-findings-appendix.md, together with the full breaking-change evaluation and the complete run-coverage record.

Breaking change: No

Findings at a glance

  • 🟡 F01 - Cursor filter dropped after page 1 — On incremental syncs the "only give me records changed since X" filter is applied to the first page of results only, and it was never established that Greenhouse carries that filter into the follow-on pages. On any of the 15 incremental streams with more than one page of changes, a sync could quietly return unrelated older records instead of the changed ones, report success, and advance its bookmark past the records it never fetched.
  • 🟡 F02 - Regression test cannot run on the shipped runtime — The single test written to protect this change only runs against an older version of the connector framework; on the version the connector actually ships with it errors out before making a single assertion. The behaviour change therefore has no working safety net on the runtime customers run.
  • 🟡 F07 - Test framework pin a major version behind what ships — The test suite's framework version was raised, but it stops a full major version short of the version the connector actually runs on, so the tests validate code that differs from what ships in exactly the area this PR rewrote. The automated check that would have flagged the mismatch did not run on this PR.
  • 🔵 F03 - Filter duplicated 15 times instead of derived from the cursor — The "changed since" filter is now written out by hand fifteen times instead of being derived from the single place that defines the sync bookmark. When the planned next phase changes the bookmark's format, those fifteen copies will silently fall out of step and the affected streams will quietly revert to pulling full history rather than failing.
  • 🔵 F04 - Dead page-size setting left in 30 places — A page-size setting is still repeated 30 times across the connector but nothing reads it any more. The next phase of this migration plans to raise the page size by editing exactly that setting, which would have no effect at all and produce no warning or test failure.
  • 🔵 F05 - Spec description still quotes the old API's rate limit — The user-facing description of the worker-count setting still quotes a rate limit that belongs to the old Greenhouse API version, even though this PR was supposed to remove that hard-coded number and already repointed the connector's rate-limit metadata link at the new API's policy.
  • 🔵 F09 - Sub-stream test bypasses the real bookmark — The test case for streams that read records per parent record (for example interviews for each application) skips the real bookmark machinery and hands the test a made-up value instead. The part most likely to break silently - and turn those streams into full-history scans - is the part not being checked.
  • 🔵 F11 - Docs link to the new API while the connector emits old-API fields — 32 stream reference links in the docs now point at Greenhouse's new API documentation, but the connector still reads the old API and still emits the old field names. A user mapping destination columns from those pages will look for fields the connector never produces - including one field that is a text status in the old API and a true/false flag in the new one.
  • F12 - Four doc links left on the retiring site — The documentation link migration was only partly applied. Activity Feed, Degrees, Disciplines and Schools still link to the Greenhouse documentation site that is being retired at the end of this month, while the stream entries listed immediately next to them were updated.
  • F15 - Test mutates global module state with no teardown — The new test registers a module globally and never removes it, so it changes how every test that runs after it in the same session resolves the connector's custom code. No harm today, but it is a latent cross-test contamination source.
  • F17 - Changelog date inversion — The changelog row for the new release is dated one day earlier than the release directly beneath it, so the version history reads as though the newer release shipped first.
Detailed findings & prescriptive fixes (for the PR author)

F01 - Incremental cursor filter is suppressed on every page after the first, and nothing establishes that Harvest v1's Link: rel="next" URL carries the filter forward (P2)

Location: airbyte-integrations/connectors/source-greenhouse/manifest.yaml:25 (and the identical line in the other 14 incremental streams: 79, 142, 202, 373, 680, 779, 830, 881, 935, 995, 1086, 1177, 1366, 1417)

Reviewer verdicts: Claude - valid (P2) / Codex - valid (P2)

Why it matters: The if not next_page_token gate is required only by Harvest v3's cursor-exclusivity rule, but Phase 1 still calls v1, whose docs never guarantee the rel="next" URL echoes created_after/updated_after/submitted_after. Gating it now buys nothing (Phase 2 renames these params to v3's created_at=gte|... form anyway) and risks page 2+ walking the unfiltered collection.

Prescriptive fix

airbyte-integrations/connectors/source-greenhouse/manifest.yaml:25 (and the identical line in the other 14 incremental streams: 79, 142, 202, 373, 680, 779, 830, 881, 935, 995, 1086, 1177, 1366 submitted_after, 1417).

Why it matters: the if not next_page_token gate is required only by Harvest v3's cursor-exclusivity rule, but Phase 1 still calls v1, whose docs never guarantee the rel="next" URL echoes created_after/updated_after/submitted_after. Gating it now buys nothing (Phase 2 renames these params to v3's created_at=gte|... form anyway) and risks page 2+ walking the unfiltered collection. Leave the per_page gate alone - it is provably safe, since v1 echoes per_page into the next link and 100 is v1's documented default.

Before (git show 4fe88e9370b3f8f1ad451047ce6d720dbf0a4b85:airbyte-integrations/connectors/source-greenhouse/manifest.yaml, lines 23-25):

          request_parameters:
            per_page: "{{ 100 if not next_page_token }}"
            created_after: "{{ stream_interval.start_time if not next_page_token }}"

After:

          request_parameters:
            per_page: "{{ 100 if not next_page_token }}"
            created_after: "{{ stream_interval.start_time }}"

This restores 0.7.32's exact wire shape for the filter: SimpleRetriever._request_params passes stream_slice on every page, so stream_interval.start_time still resolves on page 2+, and if the v1 Link URL does echo the filter, HttpClient._dedupe_query_params drops the duplicate - correct under both possible v1 behaviours. (Rejected alternative: restoring the deleted start_time_option blocks - same wire result but a 15-hunk revert instead of a 15-line edit.)

The new test must be updated to match, otherwise it fails: HttpRequest.matches compares parse_qs of the full query exactly, and unit_tests/test_pagination_request_params.py:71 currently asserts the bare link URL. Change

    second_request = HttpRequest(next_url)

to

    second_request = HttpRequest(f"{next_url}&{cursor_parameter}={cursor_value}")

(the mocked next_url should stay param-free/v3-shaped rather than being made "realistic" - see the audit appendix entry for F08, where a v1-shaped link was measured to silently disable the per_page guard.)

RECONCILIATION NOTE: if F03's fix is preferred instead, it must be applied in the form that restores start_time_option WITHOUT ignore_stream_slicer_parameters_on_paginated_requests - setting that flag reproduces exactly the page-2 suppression this finding objects to. See F03.


F02 - The only regression test for this change is written against the legacy CDK 6 stream API and cannot execute on the CDK the connector ships on (P2)

Location: airbyte-integrations/connectors/source-greenhouse/unit_tests/test_pagination_request_params.py:37-100

Reviewer verdicts: Claude - valid (P2, cdk_version_mismatch) / Codex - valid (P2, cdk_version_mismatch)

Why it matters: This is the only regression test for the PR's central change, and it cannot execute on source-declarative-manifest:7.24.0 - verified AttributeError: 'DefaultStream' object has no attribute 'stream_slices'. Driving source.read() with a ConfiguredAirbyteCatalog uses the one entry point that is stable across 6.56.7 and 7.24.0, so the test both survives the pin bump and asserts the request construction that actually ships.

Prescriptive fix

File: airbyte-integrations/connectors/source-greenhouse/unit_tests/test_pagination_request_params.py:37-100 (rewrite the test body; the parametrize block at L16-36 is replaced too).

Why it matters: this is the only regression test for the PR's central change, and it cannot execute on source-declarative-manifest:7.24.0 - verified AttributeError: 'DefaultStream' object has no attribute 'stream_slices'. Driving source.read() with a ConfiguredAirbyteCatalog uses the one entry point that is stable across 6.56.7 and 7.24.0, so the test both survives the pin bump and asserts the request construction that actually ships.

BEFORE (actual head content, git show 4fe88e9...:.../test_pagination_request_params.py, L47-100 shown):

    sys.modules["components"] = components_module
    source = YamlDeclarativeSource(str(manifest_path), config={"api_key": "test"})
    stream = next(stream for stream in source.streams(config={"api_key": "test"}) if stream.name == stream_name)
    stream_slice = (
        next(
            iter(
                stream.stream_slices(
                    sync_mode=SyncMode.incremental,
                    cursor_field=[cursor_field],
                    stream_state={},
                )
            )
        )
        if not partition
        else StreamSlice(
            partition=partition,
            cursor_slice={
                "start_time": cursor_value,
                "end_time": "2024-01-03T03:04:05.678Z",
            },
        )
    )

    next_url = f"https://harvest.greenhouse.io/v1/{path}?cursor=ABC&foo=bar"
    first_request = HttpRequest(
        f"https://harvest.greenhouse.io/v1/{path}",
        query_params={
            "per_page": "100",
            cursor_parameter: cursor_value,
        },
    )
    second_request = HttpRequest(next_url)
    cursor_context = patch.object(stream.retriever, "cursor", None) if partition else nullcontext()
    with cursor_context:
        with HttpMocker() as http_mocker:
            http_mocker.get(
                first_request,
                HttpResponse(
                    json.dumps([{"id": 1, cursor_field: cursor_value}]),
                    headers={"Link": f'<{next_url}>; rel="next"'},
                ),
            )
            http_mocker.get(
                second_request,
                HttpResponse(json.dumps([{"id": 2, cursor_field: "2024-01-03T03:04:05.678Z"}])),
            )
            records = list(
                stream.read_records(
                    sync_mode=SyncMode.incremental,
                    stream_slice=stream_slice,
                )
            )
            http_mocker.assert_number_of_calls(first_request, 1)
            http_mocker.assert_number_of_calls(second_request, 1)

AFTER (whole file; verified 2 passed on airbyte-cdk 6.56.7 AND on 7.24.0):

# Copyright (c) 2026 Airbyte, Inc., all rights reserved.

import json
import logging
import sys

import pytest

from airbyte_cdk.models import (
    AirbyteStream,
    ConfiguredAirbyteCatalog,
    ConfiguredAirbyteStream,
    DestinationSyncMode,
    SyncMode,
)
from airbyte_cdk.sources.declarative.yaml_declarative_source import YamlDeclarativeSource
from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse

_CONFIG = {"api_key": "test"}
_PARENT_URL = "https://harvest.greenhouse.io/v1/applications"
_START = "1970-01-01T00:00:00.000Z"


def _catalog(stream_name: str, cursor_field: str) -> ConfiguredAirbyteCatalog:
    return ConfiguredAirbyteCatalog(
        streams=[
            ConfiguredAirbyteStream(
                stream=AirbyteStream(
                    name=stream_name,
                    json_schema={},
                    supported_sync_modes=[SyncMode.incremental],
                ),
                sync_mode=SyncMode.incremental,
                destination_sync_mode=DestinationSyncMode.append,
                cursor_field=[cursor_field],
            )
        ]
    )


@pytest.mark.parametrize(
    ("stream_name", "path", "cursor_field", "cursor_parameter", "needs_parent"),
    [
        ("applications", "applications", "applied_at", "created_after", False),
        (
            "applications_interviews",
            "applications/123/scheduled_interviews",
            "updated_at",
            "updated_after",
            True,
        ),
    ],
)
def test_incremental_pagination_request_parameters(
    manifest_path,
    components_module,
    stream_name,
    path,
    cursor_field,
    cursor_parameter,
    needs_parent,
):
    sys.modules["components"] = components_module
    catalog = _catalog(stream_name, cursor_field)
    source = YamlDeclarativeSource(str(manifest_path), config=_CONFIG, catalog=catalog, state=[])

    next_url = f"https://harvest.greenhouse.io/v1/{path}?cursor=ABC&foo=bar"
    first_request = HttpRequest(
        f"https://harvest.greenhouse.io/v1/{path}",
        query_params={"per_page": "100", cursor_parameter: _START},
    )
    second_request = HttpRequest(next_url)

    with HttpMocker() as http_mocker:
        if needs_parent:
            http_mocker.get(
                HttpRequest(
                    _PARENT_URL,
                    query_params={"per_page": "100", "created_after": _START},
                ),
                HttpResponse(json.dumps([{"id": 123, "applied_at": _START}])),
            )
        http_mocker.get(
            first_request,
            HttpResponse(
                json.dumps([{"id": 1, cursor_field: _START}]),
                headers={"Link": f'<{next_url}>; rel="next"'},
            ),
        )
        http_mocker.get(
            second_request,
            HttpResponse(json.dumps([{"id": 2, cursor_field: "2024-01-03T03:04:05.678Z"}])),
        )

        records = [
            message.record.data
            for message in source.read(logging.getLogger("test"), _CONFIG, catalog, [])
            if message.type.value == "RECORD" and message.record.stream == stream_name
        ]

        http_mocker.assert_number_of_calls(first_request, 1)
        http_mocker.assert_number_of_calls(second_request, 1)

    assert records == [
        {"id": 1, cursor_field: _START},
        {"id": 2, cursor_field: "2024-01-03T03:04:05.678Z"},
    ]

Notes on the delta: contextlib.nullcontext, unittest.mock.patch and airbyte_cdk.sources.types.StreamSlice imports (L5-6, L12) become unused and must be dropped - the patch.object(stream.retriever, "cursor", None) hack at L79 exists only to defeat the substream cursor and is unnecessary once the real read path runs with the parent mocked. The substream case now starts at the manifest's start_datetime of 1970-01-01T00:00:00.000Z instead of the hand-built 2024-01-02... slice, because the cursor is derived rather than fabricated.

RECONCILIATION NOTE: this rewrite and F09's generate_partitions() rewrite are alternatives, not both. Land F07's pin bump to 7.24.0 in the same commit and prefer F09's version, which was measured to FAIL on two injected regressions (un-gated per_page, and partition_field_start: since); this source.read() version is the fallback if the pin has to stay at 6.56.7. Whichever is chosen, apply F01's second_request change on top if F01's fix lands.


F07 - Unit-test CDK pin bumped to 6.56.7 while the connector runs on source-declarative-manifest:7.24.0, and the CDK Version Check gate was skipped (P2)

Location: airbyte-integrations/connectors/source-greenhouse/unit_tests/pyproject.toml:13

Reviewer verdicts: Claude - valid (P2, cdk_version_mismatch) / Codex - valid (P2, cdk_version_mismatch)

Why it matters: The suite currently validates a CDK a full major behind what ships, and the two differ in exactly the RequestPath/url code path this PR rewrote - so the new regression test cannot protect the change on the runtime that runs it.

Prescriptive fix

airbyte-integrations/connectors/source-greenhouse/unit_tests/pyproject.toml:13.

Why it matters: the suite currently validates a CDK a full major behind what ships, and the two differ in exactly the RequestPath/url code path this PR rewrote - so the new regression test cannot protect the change on the runtime that runs it.

BEFORE (unit_tests/pyproject.toml:11-15):

[tool.poetry.dependencies]
python = "^3.10,<3.13"
airbyte-cdk = "6.56.7"
pytest = "^8"
requests-mock = "^1.12.1"

AFTER:

[tool.poetry.dependencies]
python = "^3.10,<3.13"
airbyte-cdk = "7.24.0"
pytest = "^8"
requests-mock = "^1.12.1"

Then regenerate the lockfile: cd airbyte-integrations/connectors/source-greenhouse/unit_tests && poetry lock.

This must land in the same commit as the test port in F09 - with the pin at 7.24.0 the current test errors out ('DefaultStream' object has no attribute 'stream_slices'), while the F09 replacement passes 2/2 at 7.24.0. (Rejected alternative: leaving the pin at 6.56.7 and adding a comment - that keeps CI green while testing code that does not ship.)


F03 - Cursor filter is hand-rolled into 15 duplicated Jinja templates instead of using the CDK's built-in ignore_stream_slicer_parameters_on_paginated_requests, decoupling the filter from the cursor that owns it (P3)

Location: airbyte-integrations/connectors/source-greenhouse/manifest.yaml:17-25 and :55-58 (and the equivalent pair in each of the other 14 incremental streams)

Reviewer verdicts: Claude - valid (P2) / Codex - overly_defensive (P4)

Why it matters: The filter value is currently a free-text Jinja literal duplicated 15 times and no longer derived from the cursor that owns it, so Phase 2's gte| prefix / format change applied to datetime_format or partition_field_start will silently diverge from these templates and degrade to an unfiltered full pull with no error. The pinned CDK already supplies the exact suppression this PR hand-rolls.

Prescriptive fix

RECONCILIATION NOTE - READ FIRST: this fix and F01's fix produce OPPOSITE page-2 wire behaviour. Setting ignore_stream_slicer_parameters_on_paginated_requests: true keeps the cursor filter suppressed on page 2+, which is exactly what F01 (P2, higher severity) objects to. If F01's fix is adopted, apply only the start_time_option half below and do NOT set the flag: that single form satisfies both findings - the filter is sent on every page (F01) and its value is bound back to the cursor with typed field_name/inject_into instead of 15 duplicated literals (F03).

airbyte-integrations/connectors/source-greenhouse/manifest.yaml:17-25 and :55-58 (and the equivalent pair in each of the other 14 incremental streams).

Why it matters: the filter value is currently a free-text Jinja literal duplicated 15 times and no longer derived from the cursor that owns it, so Phase 2's gte| prefix / format change applied to datetime_format or partition_field_start will silently diverge from these templates and degrade to an unfiltered full pull with no error. The pinned CDK already supplies the exact suppression this PR hand-rolls.

BEFORE (manifest.yaml:17-25, applications):

      retriever:
        type: SimpleRetriever
        requester:
          $ref: "#/definitions/base_requester"
          url: https://harvest.greenhouse.io/v1/applications
          http_method: GET
          request_parameters:
            per_page: "{{ 100 if not next_page_token }}"
            created_after: "{{ stream_interval.start_time if not next_page_token }}"

AFTER:

      retriever:
        type: SimpleRetriever
        ignore_stream_slicer_parameters_on_paginated_requests: true
        requester:
          $ref: "#/definitions/base_requester"
          url: https://harvest.greenhouse.io/v1/applications
          http_method: GET
          request_parameters:
            per_page: "{{ 100 if not next_page_token }}"

BEFORE (manifest.yaml:55-58, same stream):

        start_datetime:
          type: MinMaxDatetime
          datetime: "1970-01-01T00:00:00.000Z"
          datetime_format: "%Y-%m-%dT%H:%M:%S.%_msZ"

AFTER:

        start_datetime:
          type: MinMaxDatetime
          datetime: "1970-01-01T00:00:00.000Z"
          datetime_format: "%Y-%m-%dT%H:%M:%S.%_msZ"
        start_time_option:
          type: RequestOption
          field_name: created_after
          inject_into: request_parameter

Apply the same two edits to the other 14 incremental streams, restoring each stream's own field_name (updated_after for 13 of them, submitted_after for eeoc). Keep the hand-rolled per_page exactly as-is - the flag deliberately does not suppress page_size_option, and the pinned schema states plainly that "Request options set directly on the requester will not be ignored" (declarative_component_schema.yaml:4175-4177). (Rejected alternative: leaving the templates and adding a comment documenting the coupling - that keeps 15 duplicated strings the schema cannot validate.)


F04 - Removing page_size_option leaves page_size: 100 on all 30 CursorPagination strategies as dead config, a trap for the Phase 2 page-size raise (P3)

Location: airbyte-integrations/connectors/source-greenhouse/manifest.yaml:44 (and the other 29 identical lines under each CursorPagination block)

Reviewer verdicts: Claude - valid (P3) / Codex - valid (P3)

Why it matters: With page_size_option gone nothing reads page_size, and DefaultPaginator.__post_init__ only validates the reverse direction - so the Phase-2 edit page_size: 100 -> 500 will change nothing, raise nothing, and fail no test.

Prescriptive fix

airbyte-integrations/connectors/source-greenhouse/manifest.yaml:44 (and the other 29 identical lines under each CursorPagination block).

Why it matters: with page_size_option gone nothing reads page_size, and DefaultPaginator.__post_init__ only validates the reverse direction - so the Phase-2 edit page_size: 100 -> 500 will change nothing, raise nothing, and fail no test.

BEFORE (manifest.yaml:42-46):

          pagination_strategy:
            type: CursorPagination
            page_size: 100
            cursor_value: "{{ headers['link']['next']['url'] }}"
            stop_condition: "{{ 'next' not in headers['link'] }}"

AFTER:

          pagination_strategy:
            type: CursorPagination
            cursor_value: "{{ headers['link']['next']['url'] }}"
            stop_condition: "{{ 'next' not in headers['link'] }}"

Mechanically: grep -v '^ page_size: 100$' manifest.yaml removes exactly the 30 orphaned lines. The effective page size stays the per_page: "{{ 100 if not next_page_token }}" literal in request_parameters, which is the single place Phase 2 must edit to reach v3's max of 500. (Rejected alternative: keeping page_size with an explanatory comment - a comment does not stop the wrong line from being edited.)


F05 - Issue task (3) not done: the num_workers spec description still hardcodes v1's "50 requests per 10 seconds" and the rate-limit comment still cites the retiring v1 doc URL (P3)

Location: airbyte-integrations/connectors/source-greenhouse/manifest.yaml:1731-1735 (and the comment at :1689-1691)

Reviewer verdicts: Claude - valid (P3) / Codex - valid (P3)

Why it matters: The spec description this PR claims to have reworded still quotes a v1-only number to every user, and that number stops being true the moment Phase 2 repoints the endpoints - while the metadata.yaml rate_limits link this PR did change already points at the v3 policy.

Prescriptive fix

airbyte-integrations/connectors/source-greenhouse/manifest.yaml:1731-1735 (and the comment at :1689-1691).

Why it matters: the spec description this PR claims to have reworded still quotes a v1-only number to every user, and that number stops being true the moment Phase 2 repoints the endpoints - while the metadata.yaml rate_limits link this PR did change already points at the v3 policy.

BEFORE (manifest.yaml:1731-1735):

        description: >-
          The number of worker threads to use for syncing. The default is tuned
          against Greenhouse's documented limit of 50 requests per 10 seconds.
          Increase this only if your Greenhouse account can support higher API
          usage, or lower it if you see rate-limit errors.

AFTER:

        description: >-
          The number of worker threads to use for syncing. The default is tuned
          against Greenhouse's documented Harvest API rate limit. Increase this
          only if your Greenhouse account can support higher API usage, or lower
          it if you see rate-limit errors.

And in the same edit, mark the retained v1 budget comment as version-specific rather than repointing it (the 50/PT10S budget is correct for v1 and deliberately unchanged in Phase 1):

BEFORE (manifest.yaml:1689-1691):

# Greenhouse Harvest API documented rate limit (single tier - Path A):
# - 50 requests per 10 seconds (5 req/sec) for all accounts.
# - Source: https://developers.greenhouse.io/harvest.html#throttling

AFTER:

# Harvest v1 documented rate limit (single tier - Path A). Phase 1 still calls v1,
# so api_budget below intentionally stays on the v1 ceiling:
# - 50 requests per 10 seconds (5 req/sec) for all accounts.
# - Source: https://developers.greenhouse.io/harvest.html#throttling (site retires 2026-08-31)
# Phase 2 retunes to v3's 30-second fixed window:
#   https://harvestdocs.greenhouse.io/docs/api-rate-limiting

F09 - The substream test case patches the retriever cursor to None and hand-builds the StreamSlice, so the one construct most likely to fail silently is the one bypassed (P3)

Location: airbyte-integrations/connectors/source-greenhouse/unit_tests/test_pagination_request_params.py:50-68 and :79 (plus :93-98)

Reviewer verdicts: Claude - valid (P3) / Codex - valid (P3)

Why it matters: The one construct most likely to break silently - a per-partition incremental slice feeding {{ stream_interval.start_time }} - is the one the test bypasses; it was verified that adding partition_field_start: since to applications_interviews keeps the current test green while silently dropping the filter.

Prescriptive fix

airbyte-integrations/connectors/source-greenhouse/unit_tests/test_pagination_request_params.py:50-68 and :79 (plus :93-98).

Why it matters: the one construct most likely to break silently - a per-partition incremental slice feeding {{ stream_interval.start_time }} - is the one the test bypasses; it was verified that adding partition_field_start: since to applications_interviews keeps the current test green while silently dropping the filter.

BEFORE (lines 50-68, then 79, then 93-98):

    stream_slice = (
        next(
            iter(
                stream.stream_slices(
                    sync_mode=SyncMode.incremental,
                    cursor_field=[cursor_field],
                    stream_state={},
                )
            )
        )
        if not partition
        else StreamSlice(
            partition=partition,
            cursor_slice={
                "start_time": cursor_value,
                "end_time": "2024-01-03T03:04:05.678Z",
            },
        )
    )
...
    cursor_context = patch.object(stream.retriever, "cursor", None) if partition else nullcontext()
...
            records = list(
                stream.read_records(
                    sync_mode=SyncMode.incremental,
                    stream_slice=stream_slice,
                )
            )

AFTER - drop the slice construction and the cursor patch entirely, mock the parent request, and drive the real cursor via generate_partitions():

    parent_request = HttpRequest(
        "https://harvest.greenhouse.io/v1/applications",
        query_params={"per_page": "100", "created_after": cursor_value},
    )
    ...
    with HttpMocker() as http_mocker:
        if stream_name != "applications":
            http_mocker.get(
                parent_request,
                HttpResponse(json.dumps([{"id": 123, "applied_at": cursor_value}])),
            )
        http_mocker.get(first_request, HttpResponse(..., headers={"Link": f'<{next_url}>; rel="next"'}))
        http_mocker.get(second_request, HttpResponse(...))

        records = [r for p in stream.generate_partitions() for r in p.read()]

        http_mocker.assert_number_of_calls(first_request, 1)
        http_mocker.assert_number_of_calls(second_request, 1)

    assert [dict(r.data) for r in records] == [...]

The partition and cursor_value parametrize columns collapse to a single cursor_value = "1970-01-01T00:00:00.000Z" (both streams start from the manifest default), and the unittest.mock.patch, nullcontext, StreamSlice and SyncMode imports become unused. This exact shape was run under CDK 7.24.0: 2 passed on the head manifest, and it fails on both injected regressions (un-gated per_page, and partition_field_start: since). It therefore doubles as the port F07's pin bump requires. (Rejected alternative: keeping the hand-built slice and merely asserting its keys - that still never exercises the cursor that produces it.)

RECONCILIATION NOTE: generate_partitions() is 7.x-only, so this rewrite requires F07's pin bump to 7.24.0 in the same commit. Land F02/F07/F09 as one change and use this version rather than F02's source.read() variant - only this one was measured to fail on injected regressions.


F11 - 32 stream reference links repointed to Harvest v3 doc pages while all 36 streams still call v1, and several distinct streams collapse onto identical v3 URLs (P3)

Location: docs/integrations/sources/greenhouse.md:27

Reviewer verdicts: Claude - valid (P3) / Codex - valid (P3)

Why it matters: The 32 forward-looking v3 links otherwise read as a description of the records this connector currently emits, while the emitted schemas were verified byte-identical to the pre-PR (v1-shaped) versions.

Prescriptive fix

docs/integrations/sources/greenhouse.md:27 - insert a version-mismatch admonition immediately under the ## Supported Streams heading so the 32 forward-looking v3 links do not read as a description of the records this connector currently emits.

BEFORE (head, lines 27-30):

## Supported Streams

- [Activity Feed](https://developers.greenhouse.io/harvest.html#get-retrieve-activity-feed)
- [Applications](https://harvestdocs.greenhouse.io/reference/get_v3-applications) \(Incremental\)

AFTER:

## Supported Streams

:::note
The reference links below point to the Greenhouse **Harvest v3** documentation, which is where these endpoints are moving. This connector still reads **Harvest v1**, so the records it emits keep the v1 field names: `applications.applied_at` (v3 renames it to `created_at`), `users.disabled` (v3: `deactivated`), `job_openings.status` (v3: `open`, a boolean instead of a string). When mapping destination columns, use the stream schema shown in the Airbyte UI rather than the linked v3 page.
:::

- [Activity Feed](https://developers.greenhouse.io/harvest.html#get-retrieve-activity-feed)
- [Applications](https://harvestdocs.greenhouse.io/reference/get_v3-applications) \(Incremental\)

(Rejected alternative: reverting the 32 links to developers.greenhouse.io - that host is removed on 2026-08-31, three weeks out, so it trades a wrong description for 32 dead links.) :::note is the Docusaurus admonition already used across docs/integrations/sources/ (e.g. stripe.md, salesforce.md).

Scope note: the "five pairs collapse onto identical v3 URLs" half of this finding is NOT part of the fix - the vendor's own migration map sends /v1/scheduled_interviews and /v1/applications/{id}/scheduled_interviews both to /v3/interviews, and /v1/job_stages and /v1/jobs/{id}/stages both to /v3/job_interview_stages, so there is no distinct v3 page to point at.


F12 - Doc-link migration applied inconsistently: four stream links still cite the retiring v1 doc site (P4)

Location: docs/integrations/sources/greenhouse.md:29, :43, :45, :58

Reviewer verdicts: Claude - valid (P4) / Codex - valid (P4)

Why it matters: The page currently cites two doc sites for the same connector, and the four rows left on developers.greenhouse.io die when that host is removed on 2026-08-31.

Prescriptive fix

docs/integrations/sources/greenhouse.md lines 29, 43, 45, 58 - repoint the four skipped stream links to the v3 pages the vendor's migration guide names, so the page cites one doc site and no row dies when developers.greenhouse.io is removed on 2026-08-31.

BEFORE (head):

- [Activity Feed](https://developers.greenhouse.io/harvest.html#get-retrieve-activity-feed)
...
- [Degrees](https://developers.greenhouse.io/harvest.html#get-list-degrees)
...
- [Disciplines](https://developers.greenhouse.io/harvest.html#get-list-approvals-for-job)
...
- [Schools](https://developers.greenhouse.io/harvest.html#get-list-schools)

AFTER:

- [Activity Feed](https://harvestdocs.greenhouse.io/reference/get_v3-notes)
...
- [Degrees](https://harvestdocs.greenhouse.io/reference/get_v3-custom-field-options)
...
- [Disciplines](https://harvestdocs.greenhouse.io/reference/get_v3-custom-field-options)
...
- [Schools](https://harvestdocs.greenhouse.io/reference/get_v3-custom-field-options)

Leave lines 7 and 15 (the API-key setup links) and manifest.yaml:1691 on the v1 host - both are correct for a connector that still speaks v1, and the linked issue explicitly requires lines 7 and 15 to stay on v1 until Phase 2 changes auth. The Disciplines row also had the wrong v1 anchor (#get-list-approvals-for-job), which this edit incidentally retires. (Rejected alternative: leaving all four as-is until Phase 2 - that forces a second docs-only pass for four lines.)

Anchor correction: the finding's metadata.yaml:61 anchor is mis-filed - that line already reads https://harvestdocs.greenhouse.io/docs/authentication. Nothing in metadata.yaml needs to change for this finding.


F15 - sys.modules["components"] is mutated globally in the test body with no teardown (P4)

Location: airbyte-integrations/connectors/source-greenhouse/unit_tests/test_pagination_request_params.py:37-48

Reviewer verdicts: Claude - valid (P4) / Codex - valid (P4)

Why it matters: The bare sys.modules write persists past this test and silently changes how every later test in the session resolves custom components; pytest's built-in monkeypatch reverts it at teardown.

Prescriptive fix

airbyte-integrations/connectors/source-greenhouse/unit_tests/test_pagination_request_params.py:37-48 - the bare sys.modules write persists past this test and silently changes how every later test in the session resolves custom components; pytest's built-in monkeypatch reverts it at teardown.

BEFORE (head, lines 37-48):

def test_incremental_pagination_request_parameters(
    manifest_path,
    components_module,
    stream_name,
    path,
    partition,
    cursor_field,
    cursor_parameter,
    cursor_value,
):
    sys.modules["components"] = components_module
    source = YamlDeclarativeSource(str(manifest_path), config={"api_key": "test"})

AFTER:

def test_incremental_pagination_request_parameters(
    monkeypatch,
    manifest_path,
    components_module,
    stream_name,
    path,
    partition,
    cursor_field,
    cursor_parameter,
    cursor_value,
):
    monkeypatch.setitem(sys.modules, "components", components_module)
    source = YamlDeclarativeSource(str(manifest_path), config={"api_key": "test"})

monkeypatch is a pytest builtin (function-scoped, so it composes fine with the session-scoped components_module fixture) - no new dependency and no change to the pinned CDK contract. Do NOT add a components_module is None guard: components.py exists at head and the manifest's state_migrations require it, so the fixture cannot return None - that half of the finding is dropped. (Rejected alternative: an autouse fixture wrapping the assignment - more code for a single call site.) Carry this one-line change into whichever test rewrite lands from F02/F09.


F17 - Changelog date inversion: 0.8.0 dated 2026-08-10 sits above 0.7.33 dated 2026-08-11 (P4)

Location: docs/integrations/sources/greenhouse.md:81

Reviewer verdicts: Claude - valid (P4) / Codex - valid (P4)

Why it matters: The new release must not be dated before the release directly beneath it in a newest-first table.

Prescriptive fix

docs/integrations/sources/greenhouse.md:81 - the new release must not be dated before the release directly beneath it in a newest-first table.

BEFORE (head, line 81):

| 0.8.0 | 2026-08-10 | [83811](https://github.com/airbytehq/airbyte/pull/83811) | Send pagination and incremental filter parameters only on first-page requests, and use fully-qualified per-stream URLs, in preparation for the Harvest v3 migration. |

AFTER:

| 0.8.0 | 2026-08-11 | [83811](https://github.com/airbytehq/airbyte/pull/83811) | Send pagination and incremental filter parameters only on first-page requests, and use fully-qualified per-stream URLs, in preparation for the Harvest v3 migration. |

If the merge slips past 2026-08-11, use the actual merge date instead - the invariant is simply that it is >= the 2026-08-11 date on the 0.7.33 row at line 82.


Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Thanks — useful review, especially the CDK-version experiment. Triage of the 11 findings, pushed in ab87b047b7c:

Fixed

  • F05 — correct, and it was the sharpest catch: task (3) of the issue genuinely had not landed. The num_workers description still read "documented limit of 50 requests per 10 seconds"; it now references Greenhouse's documented Harvest API rate limit without the version-specific number, and the budget comment says explicitly that the 50/PT10S ceiling is v1's and that Phase 2 retunes against the v3 policy. api_budget and default_concurrency remain unchanged, per the issue.
  • F07 + F02 + F09 — agreed, and taken as one change: unit_tests CDK pin moved 6.56.77.24.0 (the version bundled in the base image), lockfile regenerated, and the test ported to the real cursor/partition machinery — generate_partitions() + Partition.read(), with the parent applications request mocked for the substream case, so the hand-built StreamSlice and the patch.object(retriever, "cursor", None) hack are both gone. Full suite: 3 passed under 7.24.0. Re-confirmed it still gates the change: with the two tested streams' request params made unconditional, both cases fail on ?cursor=ABC&foo=bar&per_page=100&created_after=... / &updated_after=....
  • F15monkeypatch.setitem(sys.modules, "components", ...).
  • F170.8.0 redated 2026-08-11.

Escalated, not changed: F01. This one is a real question rather than a nit, but the gate you're asking me to remove is the explicit requirement of the originating issue (airbytehq/airbyte-internal-issues#16900 task 2: drop start_time_option and re-express it as a conditional request parameter), so I'm not reversing it unilaterally — I've raised it with the requester. To sharpen the discriminating evidence for whoever decides: the risk only materialises if Harvest v1's Link: rel="next" URL does not echo the incremental filter. I have not confirmed either way — the CI acceptance log contains no second-page URL, as your own probe notes, and I have no live credentials to fetch one. If it does echo the filter (my expectation, unverified), the gate is a no-op on v1 and un-gating would additionally be a no-op because HttpClient._dedupe_query_params drops the duplicate; if it does not, your finding stands and page 2+ walks unfiltered. One authenticated two-page request against any incremental v1 endpoint settles it.

Not changing (with reasons)

  • F03 — as you note in your own reconciliation, ignore_stream_slicer_parameters_on_paginated_requests: true produces exactly the page-2 suppression F01 objects to, so the two fixes cancel. The remaining half (typed start_time_option instead of 15 Jinja literals) only applies if F01 is adopted; it rides along with that decision rather than landing separately.
  • F04 — accurate that page_size: 100 is now unread, but deleting 30 lines in a PR whose whole claim is byte-identical v1 output adds diff without changing behaviour, and Phase 2 is the commit that touches page size (100 → 500) anyway. Better handled there, where the change is testable in the same commit.
  • F11 / F12 — deliberately out of scope. The 32 v3 links are the issue's task (5) and are forward-looking by design; adding an admonition about v1-vs-v3 field names (F11) and repointing Activity Feed / Degrees / Disciplines / Schools onto get_v3-custom-field-options (F12) are both editorial judgements about pages I cannot verify are the vendor's intended mapping. Flagging for the reviewer rather than guessing.

Also worth carrying forward for whoever reviews: Pre-Release Checks were skipped, so no live regression / record-diff ran, and I had no Greenhouse credentials — that acceptance criterion from the issue is still unmet, as stated in the PR description.


Devin session

@pnilan

Copy link
Copy Markdown
Collaborator

I want you to independently verify F01 with Greenhouse's Harvest API documentation

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🙋 Human Input Needed (re: your request to independently verify F01) — the documentation does not answer it directly, and I want your call before changing the gate. Verdict: F01's failure mode is very unlikely to be real on v1, but the evidence for that is inference from documented pagination semantics, not an explicit vendor statement.

What the docs actually say

  • v1 pagination is offset-based. Greenhouse's own v3 migration guide describes the old scheme in the past tense: "pagination was implemented by passing page and per_page parameters. The backend would then use limit and offset to fetch the appropriate records" (https://harvestdocs.greenhouse.io/docs/step-by-step-migration-instructions). The v1 intro documents page as "a cursor for use in pagination. Returns the n-th chunk of per_page objects" (https://developers.greenhouse.io/harvest.html).
  • The documented v1 Link example echoes the caller's own parameter back into the next URL — the request used per_page=2 and the header returns <…/v1/candidates?page=2&per_page=2>; rel="next". So the next link is built from the request URL rather than from defaults.
  • Nowhere does the v1 documentation state which query parameters are carried into the rel="next" URL. There is no sentence to cite either way, and no documented example of a Link header produced by a filtered request.

Why the failure mode is nonetheless unlikely

  • Under limit/offset semantics, an offset is only meaningful relative to the filtered result set. If Greenhouse dropped updated_after from the next link, page 2 would be offset-100 into the unfiltered collection for every API consumer that filters, not just this connector — v1's own pagination contract would be broken for filtered list calls. That is a strong structural argument, though it is an argument, not a citation.
  • The one real captured v1 Link header I can find echoes the entire original query string and appends the continuation token: request …/v1/users/1372428/permissions/jobs?page=1&per_page=100Link: <…?page=1&per_page=100&since_id=133383843>; rel="next" (https://stackoverflow.com/questions/78939798). That capture has no date filter on it, so it evidences "the query string is echoed" but does not specifically prove updated_after survives.
  • Contrast with v3, which is explicitly the opposite design: the cursor "contains the information needed to paginate through the records you initially requested" and its size "may grow or shrink based on the … filters provided", with filters forbidden alongside it (https://harvestdocs.greenhouse.io/docs/pagination). Filters live inside the cursor in v3, which is exactly why the gate is required there and is a no-op there.

What would settle it, and what I cannot do: one authenticated two-page v1 request against any incremental endpoint, e.g. curl -su "$KEY": 'https://harvest.greenhouse.io/v1/candidates?per_page=1&updated_after=2026-01-01T00:00:00.000Z' -D - and read whether updated_after appears in the rel="next" URL. I have no Greenhouse credentials in this session, and CI's acceptance log contains no second-page URL (as your own probe found), so I cannot close this from here.

Recommendation. Un-gate the incremental filter for Phase 1 (F01's fix), keeping per_page gated. It is safe under both possible v1 behaviours: if the next link omits the filter, we restore correctness; if it echoes the filter verbatim, HttpClient._dedupe_query_params (which HttpRequester enables) drops the duplicate and the wire request is unchanged. The one residual wrinkle: dedupe compares values as strings, so if Greenhouse re-serialised the timestamp differently we would send the parameter twice — harmless for an identical filter value, but worth knowing. It also reproduces 0.7.32's exact wire shape, which is what this PR claims to do. Cursor-exclusivity is only enforced by v3, where filters are baked into the cursor — so the gate belongs in Phase 2 (airbytehq/airbyte-internal-issues#16901), landing together with the endpoint repoint and the gte| filter rename, where it can be validated against the API that actually requires it. The cost is that Phase 1 no longer pre-validates the gate.

Two notes if you take that route: it reverses task 2 of airbytehq/airbyte-internal-issues#16900 as written, so it is your call rather than mine; and it is best implemented as F03's start_time_option form (typed field_name/inject_into bound to the cursor, without ignore_stream_slicer_parameters_on_paginated_requests) rather than as 15 unconditional Jinja literals — that satisfies F01 and F03 in one edit. Say the word and I'll push it plus the matching test update.


Devin session

@pnilan

Copy link
Copy Markdown
Collaborator

Good callout, drop F01 as necessary.

@pnilan

Patrick Nilan (pnilan) commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

/ai-prove-fix

AI Prove Fix Started

Running readiness checks and testing against customer connections.
View workflow run
🔍 AI Prove Fix session starting... Running readiness checks and testing against customer connections. View playbook

Devin AI session created successfully!

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

👍 On it. Reading "drop F01 as necessary" as: drop the gate on the incremental cursor filter, per the recommendation above. Implementing it as the typed start_time_option on each incremental stream's cursor (F03's form, without ignore_stream_slicer_parameters_on_paginated_requests), so the filter is sent on every v1 page exactly as 0.7.32 does today, while per_page stays gated to first-page-only. Updating the second-page assertion in unit_tests/test_pagination_request_params.py to match, and I'll follow up here with the commit SHA.


Devin session

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

⚠️ Heads-up for the /ai-prove-fix run (https://app.devin.ai/sessions/13b506736a604f14b16f07471294fa66): the branch head is about to change. I'm pushing the F01 decision from the comment above — the incremental cursor filter (created_after/submitted_after/updated_after) goes back to being sent on every page via the cursor's typed start_time_option, and only per_page stays first-page-only. Please prove the fix against the commit that lands after this comment, not the current head; I'll post the SHA here as soon as it's pushed.


Devin session

@airbyte-support-bot

Airbyte Support Bot (airbyte-support-bot) commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

🟢 Fix Proven — source-greenhouse 0.8.0

The pagination change does what it claims and changes nothing else. The per_page parameter is gone from cursor/next-page requests — the thing Harvest v3 rejects with 422 — while the incremental cursor filter still goes out on every page exactly as it does today, and a real Greenhouse account produced byte-identical output to the currently published version across 2,192 records and roughly 23 next-page requests per version.

Two independent evidence sources, because the summary table alone could never prove this: the PR predicts no observable output difference, so a green comparison is necessary but not sufficient — it has to be paired with the actual per-page request shape.

1. Request shape (deterministic capture, PR head vs. master, mocked two-page responses):

page 1 page 2 (Link-derived)
0.8.0 per_page=100 + cursor filter cursor filter only — no per_page
0.7.33 per_page=100 + cursor filter cursor filter + per_page=100

Both versions walk to page 2 and emit every record from both pages, against the same host and path. So the v3-blocking parameter is removed from exactly the requests that would have failed, the cursor filter is untouched (the F01 decision landed correctly), and gating per_page does not truncate the page walk — the Link URL carries the page size itself.

2. Live regression comparison (Tier-2 customer connection, cold read, latest (auto) = 0.7.33 as control): every command ✅ on both versions, and every comparison check ✅ — Spec compatibility (one compatible change: the intended num_workers.description reword), Catalog schema unchanged across all 36 streams, Stream coverage, record counts, PK presence, PK integrity on both sides, field values, and Final state per stream.

Stream Control Target Delta
jobs 830 830 0
offers 779 779 0
users 583 583 0
Total 2,192 2,192 0

Every one of those streams exceeded the 100-record first page, so each crossed several page boundaries under the new gating and still returned the complete record set with identical PKs, identical field values and an identical state shape.

Next steps

  • Ready to merge from a regression standpoint. Worth running /ai-canary-prerelease after merge to watch organic customer syncs on the real published version before Phase 2 builds on top of it.
  • Phase 2 (airbytehq/airbyte-internal-issues#16901) is the breaking half — schema and auth — and is where the api_budget retune to v3's window belongs; nothing in this PR pre-empts it.
  • Daily hands-free triage will keep watching source-greenhouse syncs and will surface any post-merge fallout on its own.
📄 Details: connector, baseline, pre-release, evidence plan, pre-flight, full run log

Connector and PR

Connector source-greenhouse (API source, manifest-only)
Version 0.7.330.8.0 (MINOR, non-breaking)
PR #83811
Head tested 3d9e70dd02e — the post-F01 commit, cursor filters restored on paginated requests
Pre-release airbyte/source-greenhouse:0.8.0-preview.3d9e70d (publish run)
Comparison baseline latest (auto) = 0.7.33, no override_control_image
Private detailed results airbytehq/airbyte-internal-issues#16900 (comment)

Why no known-bad baseline: this is proactive v3 migration prep, not a customer-reported regression, so no version is known-bad and today's published version is the only meaningful control. No major-version boundary is crossed.

What the change actually does (net vs. 0.7.33)

  1. base_requester.url_base + per-stream path → one fully-qualified url per stream. Same v1 endpoints.
  2. page_size_option removed; per_page: "{{ 100 if not next_page_token }}" added as a conditional request parameter, so per_page is sent only on the first request of each partition.
  3. Cursor start_time_option unchanged on this head — the incremental filter still goes out on every page.
  4. num_workers description reworded; api_budget / default_concurrency untouched.
  5. allowedHosts gains auth.greenhouse.io; documentation reference URLs repointed.

Evidence plan

Hypothesis: output is byte-identical, so the comparison table should be all ✅ with no behavioral difference — the "no visible difference predicted" case, where proof has to come from the request shape.

Proving criteria (all met): page 2 carries no per_page on the PR version but does on the control; the cursor filter survives on both pages on both versions; pagination walks to completion; record counts, PKs, field values and state shape identical to control; and an affected paginated stream returns >100 records so the page-2 path is actually executed.

Disproving criteria (none observed): target emitting fewer records than control; a stream stopping after page 1; missing PKs, field-value diffs, Catalog schema ❌ or a state shape change; a target request URL differing from the control's beyond the intended per_page removal.

Strategy: regression tests only, no live pinning. source-greenhouse is a source authenticating with a Harvest API key (no forced-OAuth write-back), and the change is general request construction that any account with enough data exercises, so pinning a live customer connection would have added risk without adding signal. No connection was pinned and no customer sync was triggered — the harness reads a connection's config and catalog into an isolated CI run. Candidates were selected with exclude_pinned=True, non-EU only, internal Airbyte connections tried first.

Pre-flight checks

  • Viability ✅ — the diff matches the linked issue's tasks; the change is declarative (no new Python component), and the else-branch subtlety (per_page=None leaking as a literal parameter) was verified rather than assumed.
  • Safety ✅ — no obfuscation, no credential-handling change, no unexpected external calls. One new allowedHosts entry, auth.greenhouse.io, which is Greenhouse's own documented v3 token host and is required before Phase 2.
  • Breaking change: none ✅ — checked against the full checklist on the actual diff, not the PR's claim: no field added/removed/renamed/retyped, no PK change, no cursor-field change, no spec field added or removed (description-only edit), no stream removed, no reduced data scope, no state-format change. Independently corroborated by Spec compatibility ✅ and Catalog schema ✅ on every run that got that far.
  • Reversibility ✅ — manifest-and-metadata-only, no state or config format change, so 0.7.33 reads anything 0.8.0 writes. Changelog present. Progressive rollout is not enabled on this connector and the PR does not change that. Correct MINOR bump.
  • Design intent ✅ — the one judgement call (whether to gate the cursor filter to first-page-only as well) was raised as F01 and resolved by the maintainer above; this head keeps the filter on every page, which is 0.7.33's existing behavior, and the capture above confirms that is what the code actually does.

Full attempt log — 5 attempts, 3 lost to unrelated infrastructure

# Setup Outcome
1 Integration-test credentials, all 36 streams ✅ every check green — but ⚠️ coverage: 2 of 36 streams, 7 records total, and 33 × 403 Forbidden on both versions. Nothing paginated, so the change was never exercised. Green ≠ proven.
2 Internal Airbyte connection, all streams, cold read ⚪ inconclusive — HTTP 401 Invalid Basic Auth credentials on both versions; that connection's Harvest key is expired. Salvage: debug logging showed target and control emitting byte-identical first-page URLs, confirming the per-stream url refactor reproduces the same host, path and query string.
3 Customer connection, all 36 streams, cold read READ hit the harness's 180-minute step timeout — a full-history cold read of 36 streams including per-application substreams does not fit the step budget on a real recruiting account. SPEC/CHECK/DISCOVER ✅.
4 Same, narrowed to applications, candidates READ cancelled externally at ~47 min, well short of the timeout, artifacts never uploaded. No concurrency block on the workflow and no newer run started before the cancellation, so this was not self-inflicted; the run-attempt metadata is not visible to this token, so runner eviction vs. manual cancel can't be distinguished.
5 Same, narrowed to jobs, offers, users 🟢 fully green with real coverage — 2,192 records, all three streams past 100, target identical to control on every check.

Narrowing streams was used only on the timeout-retry path. Attempts 2-4 are inconclusive rather than failures — a failure identical on the control, a timeout, and a cancellation provide no evidence for or against the PR, and the rules treat infrastructure losses as replaced attempts. Two incidental observations, neither a regression signal: the target logged a handful of 429 backoff-and-retry cycles the control did not, which is expected when two versions run sequentially against a 50-req/10s ceiling and the retries succeeded; and no run produced HAR captures, which is why the request-shape evidence was gathered deterministically instead.


Devin session

@airbyte-support-bot Airbyte Support Bot (airbyte-support-bot) added the hyd-prove Hydra: ai-prove-fix stage has run label Aug 12, 2026
Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

☑️ Resolved in 3d9e70d. F01's gate is dropped: each incremental stream's cursor is back on a typed start_time_option (RequestOptionrequest_parameter, same field_name per stream as 0.7.33created_after / submitted_after / updated_after), so the date filter is sent on every v1 page. per_page: "{{ 100 if not next_page_token }}" is untouched on the 30 paginated streams, and ignore_stream_slicer_parameters_on_paginated_requests is set nowhere. Verified against 0.7.33: 15 start_time_option blocks with identical field names, zero conditional cursor params.

The second-page assertion in unit_tests/test_pagination_request_params.py now expects the Link URL's own params plus the cursor filter, still asserting per_page is absent — 3 passed on CDK 7.24.0. The 0.8.0 changelog line no longer claims the incremental filter is first-page-only.

This also settles F03 (typed cursor option instead of a Jinja literal). Description updated to match.

Patrick Nilan (@pnilan) — for the /ai-prove-fix run (https://app.devin.ai/sessions/13b506736a604f14b16f07471294fa66): 3d9e70d is the commit to prove against.


Devin session

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Pre-release Connector Publish Started

Publishing pre-release build for connector source-greenhouse.
PR: #83811

Pre-release versions will be tagged as {version}-preview.3d9e70d
and are available for version pinning via the scoped_configuration API.

View workflow run
Pre-release Publish: SUCCESS

Docker image (pre-release):
airbyte/source-greenhouse:0.8.0-preview.3d9e70d

Docker Hub: https://hub.docker.com/layers/airbyte/source-greenhouse/0.8.0-preview.3d9e70d

Registry JSON:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete tests, unnecessary

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 On it. Removing test_pagination_request_params.py, and reverting the unit_tests/pyproject.toml + poetry.lock changes that existed only to pin the CDK for that test — so the connector goes back to having no unit-test suite of its own. Will update the PR's ## Test Coverage section to record that the request-shape evidence now comes from the /ai-prove-fix run (deterministic two-page capture + live comparison vs 0.7.33) rather than a shipped test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☑️ Resolved in 5e7cab9. Deleted test_pagination_request_params.py and reverted unit_tests/pyproject.toml + poetry.lock to master, so the connector diff is now just manifest.yaml + metadata.yaml (plus the docs file). PR description's Test Coverage and Review guide sections updated accordingly.

Co-Authored-By: bot_apk <apk@cognition.ai>
@pnilan

Copy link
Copy Markdown
Collaborator

enable autopilot progressive rollouts for this pr

…8.0-rc.1

Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration

devin-ai-integration Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

☑️ Resolved in f2b7e49 — autopilot progressive rollout enabled.

metadata.yaml now carries:

  releases:
    rolloutConfiguration:
      enableProgressiveRollout: true
      defaultRolloutMode: autopilot
      autopilotConfig:
        autoStart: true
        autoPromoteStages: true
        strategy: fast

Since progressive rollout requires the -rc.x suffix or the pre-release checks fail, the version is re-cut as 0.8.0-rc.1 in both dockerImageTag and the greenhouse.md changelog entry. No pyproject.toml version to sync — this is a manifest-only connector. PR description updated.

One thing to flag: the /ai-prove-fix evidence on this PR was gathered against 0.8.0-preview.3d9e70d, built from 3d9e70dd02e. The two commits since then (5e7cab9d832 deleting the unit test, f2b7e493b73 this rollout config) touch no manifest logic, so that evidence still applies to the connector's runtime behavior — but the published RC image will be a different tag than the one tested.


Devin session

connectorType: source
definitionId: 59f1e50a-331f-4f09-b3e8-2e8d4d355f44
dockerImageTag: 0.8.0
dockerImageTag: 0.8.0-rc.1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't need to -rc.X tag for autopilot progressive rollouts -- log that hydra feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 On it. Reverting dockerImageTag and the changelog entry back to plain 0.8.0, keeping the rolloutConfiguration block. Then logging the Hydra feedback — the /ai-fix playbook's Step 4 currently states the -rc.x suffix is mandatory whenever enableProgressiveRollout: true and that pre-release checks fail without it, which is what I followed; that needs to be scoped to non-autopilot rollouts.


Devin session

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☑️ Resolved in a7479c7. dockerImageTag and the greenhouse.md changelog entry are back to plain 0.8.0; the rolloutConfiguration autopilot block from f2b7e49 stays. PR description updated. Logging the playbook feedback next.


Devin session

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☑️ Hydra feedback logged as well — missing_guidance against the issue_fix playbook, attributed to you, with auto-triage dispatched: https://github.com/airbytehq/airbyte-ops-mcp/actions/runs/31567133927

The specific text to fix is devin/playbooks/issue_fix.md lines 68-77 in airbytehq/ai-skills, which currently says the -rc.x suffix is required whenever enableProgressiveRollout: true and that pre-release checks fail without it — no carve-out for defaultRolloutMode: autopilot. That needs scoping to manual rollouts.


Devin session

… need it

Co-Authored-By: bot_apk <apk@cognition.ai>
@pnilan
Patrick Nilan (pnilan) marked this pull request as ready for review August 12, 2026 05:37
@pnilan
Patrick Nilan (pnilan) enabled auto-merge (squash) August 12, 2026 05:38
@pnilan

Copy link
Copy Markdown
Collaborator

Once this PR merges, close the linked github issue

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

👍 On it (re: closing the linked issue on merge).

Tracking merge on airbytehq/airbyte-internal-issues#16900 and will close it with a summary comment once this lands. The GitHub event-subscription backend is currently down (SUBSCRIPTION_API_URL environment variable is not set), so I've set a recurring check instead of a merge webhook — first check tomorrow 09:00 PT, re-armed until the PR merges. If this merges and I go dormant before the check fires, ping this session and I'll close the issue immediately.


Devin session

@pnilan
Patrick Nilan (pnilan) merged commit 722558a into master Aug 12, 2026
63 of 68 checks passed
@pnilan
Patrick Nilan (pnilan) deleted the devin/1786404163-greenhouse-v3-phase1 branch August 12, 2026 05:40
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

☑️ Resolved. This merged, so airbytehq/airbyte-internal-issues#16900 is now closed as completed, with a wrap-up comment covering all five Phase 1 tasks, the two spec corrections (the literal-'None' interpolation and the reversed cursor-filter gating), the prove-fix acceptance evidence, and the handoff to Phase 2 (airbytehq/airbyte-internal-issues#16901): https://github.com/airbytehq/airbyte-internal-issues/issues/16900#issuecomment-5262839564

One item still needs a human: #83323, the earlier draft PR adding two Harvest v3 externalDocumentationUrls entries to this same metadata.yaml block, is fully superseded by this merge and should be closed.


Devin session

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

connectors/source/greenhouse hyd-fix Hydra: ai-fix stage has run hyd-prove Hydra: ai-prove-fix stage has run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants