Skip to content

v0.9.0

Latest

Choose a tag to compare

@github-actions github-actions released this 06 Oct 02:43
c5ecddf

Added

  • DatasetRef.to_dict() (#784): a dataset reference serialises to a JSON-safe dict with a stable set of keys — identity, status (#783), query_support, license, request_parameters, application and verified_at. dataclasses.asdict(ref) raised TypeError on raw_metadata, so a consumer had to read raw_metadata, which carries no stability promise. Every key is always present; None means nothing is declared. The key list is in docs/compatibility.md §3. kpubdata datasets list --format json and datasets show --format json print this dict: list entries gain the new keys next to id, name, provider and operations, and show keeps capabilities and raw_metadata_keys beside them.
  • kpubdata.SENSITIVE_PARAM_KEYS (#782): the names kpubdata masks as credentials — servicekey, service_key, api_key, apikey, token, authorization, secret, password, key, oc, accesstoken, consumer_key, consumer_secret — are importable from kpubdata and kpubdata.transport. They lived only in the private kpubdata.transport._sensitive, so Builder kept its own list, and that list lacks key, oc, consumer_key and consumer_secret. Names are casefolded and matched exactly; a release only adds names. Value-based masking stays private.
  • DatasetRef.status (#783): a dataset reference says how far the dataset has been verified, as a DatasetStatus (now exported from kpubdata). A dataset awaiting an application, one checked against fixtures only and one verified against the live API used to look the same at run time; the levels lived only in SUPPORTED_DATA.md, which is not in the wheel. scripts/gen_dataset_status.py writes src/kpubdata/dataset_status.json from that document's level column (through SUPPORTED_DATA_LEVELS, with a spec's status: override applied), and tests/unit/test_dataset_ref_status.py fails when the two differ. Of the 155 datasets the table and the runtime share: 94 application_required, 36 fixture_verified, 24 live_verified, 1 unstable (datago.ocean_buoy, by its spec's override); datago.generic is not in the table and reports None. The status is the recorded one, not a live check — that is Client.probe.
  • Evidence bound to the spec, the commit and the run (#522, #713): a recorded fixture's meta.json now carries spec_sha256, a digest of the spec file's pipeline-relevant content (kpubdata.core.spec.spec_file_digest, which normalises away the last_verified line the recorder itself rewrites), and record_commit/run_ref from the CI environment (GITHUB_SHA/GITHUB_RUN_ID) when present. make verify gains a spec-binding step: a fixture whose recorded digest no longer matches its spec is void — the spec changed after recording — and fails with re-record guidance. Fixtures recorded before the digest existed are reported as legacy evidence rather than failed — since #717, only those on a frozen baseline.
  • Public probe API (#694): Client.probe(dataset_id) -> ProbeResult | None and Client.probe_all(*, provider=None) -> list[ProbeResult] classify reachability with the client's own keys, and ProbeResult and PROBE_STATUSES (the ADR 0005 ProbeStatus vocabulary) are exported from kpubdata. Probing keeps its fast-fail transport (PROBE_TIMEOUT_SECONDS, PROBE_RETRIES, no cache), and a run shares one transport that is closed at the end — previously every dataset opened its own and none was closed. kpubdata probe now goes through these methods; its output and report are unchanged, and it now honors the global --provider-key option, which it silently ignored before.
  • Explicit-keys-only mode (#694): Client(..., env_keys=False) (backed by KPubDataConfig.env_fallback) uses only provider_keys and never reads a credential from the environment — not KPUBDATA_<P>_API_KEY, <P>_API_KEY or KPUBDATA_SGIS_CONSUMER_SECRET. A service probing with a user's key no longer has a missing entry silently filled with the operator's key. With no key the probe makes no call and reports auth_unknown. The default (env_keys=True) is unchanged.
  • R3 review gate (#722, kpubdata-builder#905): a pull request labelled review:R3 now needs an approval from someone other than its author before it can merge. scripts/r3_review.py, wrapped by the .github/actions/r3-review composite action, is the one implementation all four repositories call; the R3 review workflow runs it on every pull request (passing when it is not R3) and again on label changes, pushes and review submissions or dismissals, with pull-requests: read only. Each reviewer's latest approving or change-requesting review decides; approvals on an older head count, as with branch protection's stale-approval dismissal off; the author, bots and accounts without write access do not count. POLICY 14.1 and AGENTS.md describe it. R3 review becomes a required status check next to CI gate; the approval count in branch protection stays at 0.
  • Probe evidence fields (#625, #710): ProbeResult carries http_status (200 on success — the executor raises on anything else — and the raised error's status otherwise), result_code (read off the raised error; a successful query does not parse the envelope), latency_ms, schema_hash (a fingerprint of the returned field names — data changes daily, field names are the contract) and classification (the DriftClassification this outcome feeds the dataset status machine), each None where it could not be observed — never guessed. The report envelope records who ran the probe (runner; the nightly workflow passes github-actions).
  • Nightly drift detection (#625, #711): scripts/drift_detect.py runs the dataset status machine over two consecutive probe reports — a differing schema_hash emits SCHEMA_CHANGED itself (it is not a probe outcome), transient failures move nothing before the third consecutive night, restores restore previous_status and file nothing, RETIRED moves nothing but files. The live-probe workflow grows a drift job that chains the previous night's artifacts, keeps streak state in a drift-state artifact, and files one [drift] issue per transition that needs a person — skipping datasets that already have an open one, with the status_history.json entry riding in the issue body. The exit status is always 0: an upstream failure is a drift event, not a CI failure.
  • Production-grade gate (#463, #711): scripts/check_production_grade.py evaluates the machine-checkable criteria of docs/production_grade.yaml — source and listing gate every spec; a recorded fixture (any *.meta.json: the recorder names fixtures after the example, tour_kor_area records seoul_restaurants.meta.json) and an example script gate the verified tier only (an in-progress row gates nothing — ocean_buoy's missing fixture is #627, not a surprise); fields_declared is reported, not gated. Its unit test is the CI gate: every spec dataset in the repository must pass.
  • Example recency windows (#734, #750): a spec can declare params[].max_age_days, and make verify fails an example outside the window before the live call does — naming the cause (expired, or a future issue that does not answer yet — the #731 morning trap) and the refresh route. Parameters without a window emit nothing. The relative-time representation the issue considered first was rejected: replay matches on recorded literals, and issue-slot granularity would leak provider specifics into the generic schema.
  • Recorder run reference (#523, #729, #739): the evidence-authorship gate grows a second layer — a changed meta.json by a recorder name must also carry record_commit and run_ref, and run_ref must resolve to a run of this repository's Build Dataset workflow whose head SHA is exactly that commit (gh run view; any lookup failure fails closed). A local tool committing under the recorder's name — the #728 path — no longer passes. ci.yml gains actions: read and the job token for the lookup.
  • Unconfirmed terms are not redistributable (#732): a spec whose licence says redistribution: allowed must carry the attribution text that proves the terms were confirmed from the provider page (#525); terms nobody checked say redistribution: unknown — the two claims #728 wrongly shipped (bus_arrival, social_enterprise) were fixed to unknown first (#740, #744), because Builder's publish gate (kpubdata-builder#892) reads exactly this flag. make verify enforces the rule with a shrink-only baseline, scripts/unconfirmed_terms_baseline.txt, frozen at the 16 legacy violators — mirroring the legacy evidence ratchet (#717): a listed spec passes while its terms stay unconfirmed, an unlisted violation fails verify, and a stale entry (terms confirmed, or the claim withdrawn) must be removed from the list.
  • PR title blocks the merge (#741): PR title joined CI gate and R3 review as a required status check in all four product repositories, so a failing title check now closes the merge button instead of only showing a red X. The check re-runs on title edits (titles.yml), which ci.yml cannot see, so it is registered in branch protection rather than added to the aggregate gate's needs. scripts/check_required_checks.py verifies the required list against the workflows (the studio#416 lesson), and POLICY 14.2 records the rule.
  • Unjustified plain http is refused (#738): a spec whose endpoint.base_url says http:// must carry endpoint.insecure_http_reason — the service key rides the query string, and over plain http anyone on the network path reads it. make verify gains a transport-security step enforced with a shrink-only baseline, scripts/insecure_http_baseline.txt, frozen at the 23 legacy http specs — all of them on apis.data.go.kr, which answers https with identical envelopes (the reproducible record — commands and raw envelopes for every service path behind the baseline — sits as a comment on #738), so each entry leaves the list by switching schemes (a switch moves the spec digest, so the fixtures are re-recorded through the Build Dataset workflow).
  • The agent PR may shrink the insecure-http baseline, and nothing else (#738): the Build Dataset commit step stages scripts/insecure_http_baseline.txt next to the spec and the fixtures, because an https conversion must land with its baseline line removed or make verify fails the ratchet. scripts/check_baseline_shrink.py guards the staging: the diff may delete exactly the converting dataset's line, add nothing, and only when that spec's base_url now says https:// — an addition, a swap or an unearned removal stops the run with needs-human, so the agent cannot widen a count-only ratchet through its own pull request. Negative tests pin each refusal; the base-branch comparison those ceilings really want is #766.

Changed

  • A request that carries a credential is not redirected (#816, decided in #812): the transport followed every redirect, so a request with the key in its query string was re-sent — key and all — to wherever the answer pointed, and over http:// anyone on the path can write that answer. Such a request is now sent without following redirects; a 3xx answer raises TransportError naming the target host (never the location, which can carry the key). A request without a credential is redirected as before. Behaviour change: a provider that redirects a keyed request in normal operation now fails; TransportConfig(follow_credentialed_redirects=True) restores following for it. No provider was checked live for this.
  • A release pull request no longer fails the compatibility check it cannot satisfy (#780). tests/unit/test_compatibility_json.py required the package's version to be inside the supported range of compatibility.json. That row is Builder's pin, and Builder pins only a released kpubdata, so raising the version to 0.9.0 failed the test unless the row claimed a pin Builder does not have yet. The test now holds what can be true at every point: the version is never behind the supported range. compatibility.json says when the row moves — with the Builder and Studio release, as docs/compatibility.md §5.1 already orders it — where its update_policy said "on every kpubdata release".
  • Unconfirmed terms are reported as unknown (#813, decided in #812): a spec that says redistribution: allowed without the attribution text that shows its terms were confirmed reported allowed on its DatasetRef. It now reports unknown — on ref.license, ref.to_dict() and raw_metadata["license"]. Behaviour change: the 16 specs frozen in scripts/unconfirmed_terms_baseline.txt no longer read as redistributable to a direct user of kpubdata; the three with attribution (datago.apt_rent, datago.apt_trade, datago.village_fcst) are unchanged. find_spec(...).license still returns what the spec file declares.
  • list_all() on a spec dataset holds one page in memory, not the whole result (#789). Global column casting (#481) needs every page before any is cast, and the fetched pages waited in a list, so memory grew with the row count: 40 pages of 500 rows peaked at 16.5 MB against 1.7 MB for 4 pages. Each page now goes to a temporary file and only its evidence for the casting decision is kept; the pages are read back, cast and yielded one at a time once the last is in — 1.3 MB for 4 pages and for 40. The casting rule, the batches and their reports are unchanged, and SpecExecutor._finalize_casting now uses the same decision code. What a failing page does is now documented, not changed: the exception propagates, no batch is yielded and the earlier pages are discarded.
  • The package metadata names its repository (#791): [project.urls] gives the homepage, documentation, repository, issue tracker and changelog under kpubdata-lab/kpubdata, so the PyPI page links back to the source. There was no [project.urls] at all.
  • compatibility.json says what it is (#788). Its description claimed "Builder and Studio CI read this file to check their kpubdata dependency range"; no workflow or script in kpubdata, kpubdata-builder or kpubdata-studio reads it. The sentence is replaced: the file is a record for people and release notes. tests/unit/test_compatibility_json.py checks what can be checked without another repository — the file's shape, that exactly one range is supported and the package's own version is inside it, and that the range is the pin docs/compatibility.md states.
  • Only a dataset that declares its terms can be published publicly (#785). 25 of the 156 datasets declare a license — exactly the spec-backed ones — and Builder's publish gate reads license.redistribution, so the other 131 gave it nothing to read. The decision, recorded in docs/policy/terms-matrix.md: they are not filled in from a provider-level default, DatasetRef.license stays None (unknown) for them, Builder refuses a public publish of such a source with redistribution_unknown, and a dataset becomes publishable when it is migrated to a spec with terms confirmed from its provider page. tests/unit/test_declared_terms_decision.py holds that every dataset with terms is spec-backed and no catalogue-only dataset has any.
  • HTTP 401 raises AuthError and HTTP 503 raises ServiceUnavailableError (#786). Both used to leave the transport as a plain TransportError, told apart only by status_code; 429 was already RateLimitError. Behaviour change: AuthError is not a TransportError, so except TransportError no longer catches a 401 — catch AuthError (or PublicDataError). A 503 is still a TransportError (its subclass), still retried, and typed once the retries run out. The kpubdata CLI exits 3 instead of 4 on a 401. Client.probe reports a 401 as auth_unknown, as before. Every error now has a stable code (auth_error, service_unavailable, rate_limited, …) and to_dict(), a JSON-serialisable dict of code, type, message, provider, dataset_id, operation, status_code, provider_code and retryable, so a consumer maps an error without comparing message text; the table is in API_SPEC.md §7.
  • Breaking: Client refuses an unknown keyword argument (#781). Client(**extra) accepted any keyword and dropped it without an error or a warning — Client(timeoutt=5) built a client with the default timeout, and env_keys=False passed to 0.8.0, which does not have it, left the environment keys in use (#780). It is now a TypeError naming the argument. Client.from_env(extra=...), whose dict was forwarded into the same channel, is removed with it; from_env takes provider_keys, timeout, max_retries, cache and cache_ttl_seconds. Nothing in the library read the dropped values, so a caller passing only documented options is unaffected.
  • The repository moved from yeongseon/kpubdata to kpubdata-lab/kpubdata. Links, the documentation site (https://kpubdata-lab.github.io/kpubdata/) and the shared GitHub Actions references now use the new owner.
  • A pull request title now blocks the merge when it breaks POLICY 2.1.3 (#741). On top of #742's Hangul and inline issue-number rules, scripts/conventional_title.py --pull-request refuses issue and pull request URLs and titles over 100 characters. GitHub's Revert "…" title is now exempt before any rule, as it should have been: it quotes the reverted squash commit, whose title ends in its PR number, so undoing a merge with GitHub's button used to fail the check. The PR title check reads the title from the API instead of the event payload, and becomes a required check in all four repositories; it is not folded into CI gate so that a title edit does not re-run the whole CI. The squash commit body is now the PR body (2026-10-01), so POLICY 2.1.3, AGENTS.md and CONTRIBUTING say commit titles (= PR titles) are English and commit bodies (= PR bodies) are free (#743).
  • A required check that failed no longer keeps a pull request blocked after a later run of the same check passes (#759). Each event starts its own check suite, and branch protection read a failed suite as failing even after another passed, so R3 review stayed blocked after an approval until someone re-ran the run the label had failed. required-check-refresh.yml re-runs the failed and cancelled runs of R3 review and Titles on the same head once one passes; both read the pull request live.
  • CODEOWNERS covers every ratchet baseline with one pattern (#764): /scripts/*_baseline.txt replaces the single legacy-evidence entry, so the unconfirmed-terms (#732) and insecure-http (#738) baselines sit behind an owner too — a count-only ratchet cannot stop one entry being swapped for another, which is why #723 put the first baseline there. tests/unit/test_codeowners.py checks the reverse direction as well: every scripts/*_baseline.txt file must match an owned pattern, so a baseline added without coverage fails a test instead of silently missing review.
  • The baseline ratchets compare sets against the base branch, not counts against frozen ceilings (#766): a count-only ratchet lets a shrunk list grow back toward its ceiling — filling the freed slots with exactly the violations it exists to stop — and a one-for-one swap never moved the count at all; #765 gives the dataset agent a path to try exactly that. verify_spec.py reads each baseline at an explicit ref — KPUBDATA_BASELINE_BASE: the pull request's base branch in ci.yml, HEAD on the Build Dataset runner (it verifies uncommitted edits on main), origin/main as the Makefile's local default — fails any entry the ref does not carry, and fails closed when the ref is missing or unreadable; there is no implicit HEAD fallback, which in a pull request's CI would compare the PR against itself. A baseline the ref does not carry reads as the empty set, so a creating change may only land it empty — freezing existing violations is the failure that names its human-reviewed moment. The frozen-size constants (LEGACY_CEILING and friends) and their count tests are gone — the ref's entry set is the ceiling, and an after-shrink addition or a swap now fails for all three baselines (#717, #732, #738).
  • The https evidence is recorded, and the transport rule runs outside make verify (#767): the insecure-http gate's citations pointed at "the keyless probe in the issue", but the issue held a results table without commands or raw envelopes, and 10 of the 16 service paths behind the baseline — ocean_buoy, the three localdata specs, six of the seven RTMS trade services — had never been probed at all. A reproducible record now sits on #738: the exact request per path over both schemes, byte-identical envelopes for all 16 (code 30 from ten real services, code 12 at the gateway for the rest), and no service needing insecure_http_reason. The CHANGELOG, the baseline header and the verify comment point at it, and the rule moved to kpubdata.core.spec.insecure_http_problem — shared by verify_spec.py and validate_spec.py the way #725 shares the licence contradictions — so an unjustified http:// base_url fails validate_spec (the CI spec validation) and not only make verify, with the same shrink-only baseline exemption on both paths.
  • CODEOWNERS covers the evidence path (#715): scripts/record.py, scripts/verify_spec.py, scripts/check_fixture_authorship.py, the recorded fixtures and the whole src/kpubdata/specs/ tree (previously only schema.json) now need a code owner's review, because whoever changes them changes what "verified" means. tests/unit/test_codeowners.py checks glob patterns too: a wildcard entry must match at least one file, since GitHub silently ignores a pattern that matches nothing. Enforcing code-owner review in branch protection stays a person's decision (POLICY 14).
  • Release rule (#685): kpubdata now releases on demand — when a downstream repository is blocked, for a security fix, or for accumulated changes — at most once every seven days, and is no longer part of the monthly release week, which stays for kpubdata-builder and kpubdata-studio. scripts/release_window.py, wrapped by the .github/actions/release-window composite action, is the one implementation: on-demand refuses a release less than seven days after the last final GitHub Release, monthly refuses outside the Monday-to-Sunday week holding the month's last Thursday, both in KST, and a critical patch passes either only when it names its issue (critical_patch/critical_issue inputs, or a Critical-Patch: #N line in the release pull request). release.yml runs it first on both the prepare and the release path; a dry run reports the decision without stopping. docs/compatibility.md §5.1, AGENTS.md and POLICY say the same.
  • bus_arrival and social_enterprise migrated to specs (#409, #728): both leave the catalogue for spec YAML with recorded live evidence. User-visible: datago.bus_arrival serves over https and reports no pagination (a station query is one page — previously offset-style, max 1000), and datago.social_enterprise caps page_size at 100 (previously 1000) and declares its PII columns (CEO name, phone, fax) per #693.
  • ultra_srt examples renamed with a fresh issue slot (#731, #736): seoul_1530/seoul_1500 → seoul_0600 (base 2026-10-01 06:00) — the KMA APIs answer only recent issues, so the old example dates had drifted outside the window and every live call reported params_invalid. Replay matching and the example scripts follow the names; the legacy evidence baseline shrank 35 → 32.

Fixed

  • Every provider request passes its credential's value for masking (#810): after #805, seven adapters (kipris, korean, kosis, law, lofin, neis, sgis) still relied on the parameter name alone. Their names are all on the sensitive list, so nothing leaked; they now pass the value too, and a test fails when a transport request under providers/ or core/ is added without it.
  • list_all no longer fails on a lone surrogate (#804): pages are spooled to a temporary file as JSON lines (#789), and a row whose text held a lone surrogate — what a broken character in a provider's response decodes to — serialised but could not be written, so the whole read raised UnicodeEncodeError where list would have returned the page. The spool is now written and read with surrogatepass, and the rows come back unchanged.
  • A key sent under an unlisted parameter name is masked (#805): the spec executor and the legacy datago adapter relied on the parameter's name alone, so a key sent under a name outside SENSITIVE_PARAM_KEYS — a spec's own auth.param_name, or datago.generic's _service_key_param — appeared in the traceback of a failed request. Both now pass the key's value to the transport, as bok, fds and seoul do, and so does the shared localdata/semas request.
  • A total count of zero is 0, not None (#806): the hand-written adapters reported RecordBatch.total_count as None whenever the provider's count was zero, so "the provider said there are no rows" could not be told from "the provider did not say". datago, the datago family (localdata, semas), lofin, law, bok, korean, neis and fds now report 0 when the response carries a zero and None only when it carries no count (or one that is not a number). Behaviour change: code that tests total_count is None to detect an empty result must test not batch.items or total_count == 0 instead. Paging is unchanged, and the spec-based path already kept the two apart.
  • API_SPEC.md's examples for datago.apt_trade pass LAWD_CD and DEAL_YMD, the names the spec declares (#790). They passed lawd_code and deal_ym, which the library sends to the provider verbatim, so the documented call could not work. tests/unit/test_doc_example_params.py reads the Python examples of API_SPEC.md, the READMEs and docs/quickstart.md and fails when a call on a spec-backed dataset passes a filter its spec does not declare.
  • NODATA is an empty result on every path (#787): data.go.kr's resultCode 03 (NODATA_ERROR, no record matched the request) came back as an empty batch from localdata and semas (#470) but raised ProviderResponseError from the datago envelope parser (the standard, gyeonggi_msg and its_flat envelopes) and from every spec dataset, since check_payload_error had no branch for it and no spec lists it in ok_values. All of them now return an empty result — items == [], and total_count as the response states it. Behaviour change: code that caught ProviderResponseError with provider_code == "03" to detect an empty answer now gets the empty batch instead; every other code raises as before.
  • The R3 review failure message cites POLICY 14.1, the section that defines the gate, instead of sections 14 and 25 (#722).
  • A KOGL licence may no longer waive attribution (#725): every KOGL type requires it, so attribution_required: false under 공공누리_1유형–4유형 now fails to load, like the commercial-use and modification contradictions #719 added. The rule now lives in one public function, kpubdata.core.spec.licence_conflicts, which the loader and scripts/validate_spec.py both call — before, validate_spec.py checked only the schema and field contradictions, so an external spec author saw a KOGL contradiction only when the spec was loaded.
  • datago.air_quality's licence contradicted itself: its attribution and redistribution: forbidden follow the source's KOGL type 3 (attribution, no modification), but it said type: 공공누리_1유형 and modification_allowed: true. It now says 공공누리_3유형 and modification_allowed: false (#719). A spec whose KOGL type contradicts an explicit commercial_use: true (types 2 and 4) or modification_allowed: true (types 3 and 4) now fails to load, so a consumer reading the type and one reading the flag cannot be told two different things.
  • A missing or null spec digest no longer passes as legacy evidence (#717). #713 (#522) let any fixture whose meta had no non-empty spec_sha256 through make verify as legacy, so deleting the key or setting it to null/"" unbound a fixture from its spec in one edit, and scripts/record.py wrote "spec_sha256": null when it could not read the spec. Legacy is now the frozen baseline scripts/legacy_evidence_baseline.txt — the 28 fixtures tracked without a digest — and only a listed fixture whose meta has no spec_sha256 key passes unbound; any other missing, null or "" digest fails the spec-binding step. The baseline is a ratchet: it fails when it grows past 28, repeats an entry, or keeps an entry that no longer exists or now carries a digest. scripts/record.py stops before calling or writing anything when it cannot compute the digest. The baseline is code-owned like the recorder and verifier.
  • The release workflows after #683 (ref validation): publish-pypi.yml's tag pattern was written \\., which grep reads as a literal backslash, so no version tag matched and every dispatched publish failed — it is one pattern now, v0.8.0 and v0.7.1a1 match and v0.8 does not. The ref is validated on the release: published path too, reaches the shell through the environment instead of ${{ inputs.ref }}, must be a tag in this repository and must be on main (git merge-base --is-ancestor). In release.yml the merge path only fires for pull requests into main, a re-run after the tag was pushed but gh release create failed finishes the release when the tag is on the same commit instead of stopping at "tag must not exist", and the release concurrency group moved from the workflow to the two jobs, so ordinary pull requests closing no longer queue in it and cannot cancel a waiting release. #683 itself had no CHANGELOG entry: it makes a release run only from main and validates the publish ref.
  • datago.bus_arrival's licence and code columns (#732, #733, via #740): the licence drops its unconfirmed 공공누리_1유형 type and says redistribution: unknown until the terms are confirmed from the provider page, and six ID columns (stationId, routeId, routeDestId, vehId1, vehId2, routeTypeCd) are text with semantic_kind: code — a column named like a request parameter is not a number (ADR 0006).

Security

  • Secret scan (gitleaks) no longer fails on another branch's commits (#774). Without --log-opts, gitleaks runs git log --all, which — combined with fetch-depth: 0 pulling in every remote branch — walked unmerged PR branches too, so a synthetic test key on someone else's open branch turned this job, which CI gate needs, red for runs that never touched that branch. The job now scopes --log-opts to the run's own commits: a pull request scans base..head, a push to main scans before..sha (or just sha for a brand-new branch, where before is all zeros), and schedule/workflow_dispatch scan the default branch's full history only — never another branch. .gitleaksignore is unaffected. Ported from kpubdata-studio#685/#686.
  • Configured keys are masked out of ProbeResult.detail by value (every percent-encoded form) before the message is truncated, so a key cut at the boundary cannot survive as a prefix (#694).
  • httpx's own INFO line HTTP Request: GET <url> ... carried the query string, and with it a data.go.kr serviceKey, verbatim. A filter on the httpx logger now masks the credential parameters named in SENSITIVE_PARAM_KEYS (#694).

Documentation

  • Docs follow the 2026-09-30 decisions (#695): the compatibility page names the three products and the one-way dependency (Studio → Builder → KPubData) and moves the main row to kpubdata 0.8.0 with Builder's >=0.8.0,<0.9 pin; TERMINOLOGY says Builder owns the Access vocabulary; RELEASE_POLICY.md points to §5.1 for cadence, adds the release-window step, and states the real latest releases; PACKAGING describes the window gate, the manual release-PR step while Actions cannot open PRs, and re-runs after #687; SECURITY no longer says versions and tags are being reconciled.

Full changelog: v0.8.0...v0.9.0