Skip to content

ci(docsbase): versioned connector docs + config schemas image, auto-refreshed on release - #402

Open
shirshanka wants to merge 1 commit into
masterfrom
ci-connector-docsbase
Open

ci(docsbase): versioned connector docs + config schemas image, auto-refreshed on release#402
shirshanka wants to merge 1 commit into
masterfrom
ci-connector-docsbase

Conversation

@shirshanka

@shirshanka shirshanka commented Aug 1, 2026

Copy link
Copy Markdown

Summary

  • Adds connector-docsbase.yml: for each acryl-datahub four-part release (X.Y.Z.W), checks out the tag, runs docGen (config schemas) and captures metadata-ingestion/docs/sources, then assembles a single versioned corpus image (/opt/<version>/{docs,schemas} layout) published as acryldata/datahub-ingestion-docsbase, tagged by the <floor>-<ceiling> version range it covers plus a moving :latest.
  • Coverage is recorded both in an in-image CORPUS_MANIFEST.json and as OCI labels (org.acryl.docsbase.floor/ceiling/versions_count/source_sha/generated_at) so a consumer can inspect what a given digest covers before pinning it.
  • Adds metadata-ingestion/scripts/docsbase/list_versions.py, which queries PyPI for final four-part acryl-datahub releases within a lookback window (used to build the version matrix for workflow_dispatch backfills).
  • Wires connector-docsbase.yml up as a workflow_call target from publish-pypi-release.yml: once a release's push_to_pypi job succeeds, and only for a genuine final X.Y.Z.W tag (rc/dev-suffixed tags are skipped), it re-runs the docsbase build for just that version and publishes it (non-dry-run) — so the image keeps itself current without anyone needing to remember to trigger it by hand.
  • workflow_dispatch remains available for manual/backfill runs (explicit versions list or a days lookback), with a dry_run input that builds without pushing.

Test plan

  • Manual workflow_dispatch dry run on this branch (single version, no push) completed successfully end-to-end: version selection → docGen → corpus assembly → image build. https://github.com/acryldata/datahub/actions/runs/30608823305
  • Confirm DOCSBASE_DH_TOKEN is configured as a repo secret before merge (needed for the real, non-dry-run push path).
  • After merge, verify the next tagged release triggers refresh_docsbase in publish-pypi-release.yml and successfully pushes an updated image.

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@shirshanka
shirshanka force-pushed the ci-connector-docsbase branch from 6a0463e to 45af722 Compare August 1, 2026 18:39
Comment on lines +197 to +205
- name: Log in to Docker Hub
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.dry_run == false }}
uses: docker/login-action@v3
with:
username: acryldata
password: ${{ secrets.DOCSBASE_DH_TOKEN }}

- name: Push
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.dry_run == false }}

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.

Blocker: github.event_name is never workflow_call here, so nothing is ever pushed.

In a called workflow the github context describes the caller's event. publish-pypi-release.yml triggers on release: published, so at this point github.event_name == 'release' — and this guard, the identical one on Push (L205), and the one on Dry-run notice (L223) are all false on the only automated path.

Net effect: a full docGen runs per version, the image is built, nothing is pushed, and the job reports success with no log line explaining the skip.

Second issue in the same block: DOCSBASE_DH_TOKEN is declared required: false (L50-52) but the caller hardcodes dry_run: false, so this login always runs on a release with no emptiness guard. docker-unified.yml:120-125 doesn't put secrets.X != '' in an if: (the secrets context isn't available there) — it evaluates it in env: and exports an output. This matters for the datahub-project/datahub mirror too, which receives this file via sync-upstream.yml and will never have the secret.

The suggestion drops the event sniffing, adds the repo's guard pattern, and pins the action. Note it also moves docker/login-action v3 → the v4.0.0 SHA the rest of the repo pins.

Suggested change
- name: Log in to Docker Hub
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.dry_run == false }}
uses: docker/login-action@v3
with:
username: acryldata
password: ${{ secrets.DOCSBASE_DH_TOKEN }}
- name: Push
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.dry_run == false }}
- name: Check whether docker login is possible
id: docker-login
env:
ENABLE_DOCKER_LOGIN: ${{ secrets.DOCSBASE_DH_TOKEN != '' }}
run: echo "docker-login=$ENABLE_DOCKER_LOGIN" >> "$GITHUB_OUTPUT"
- name: Log in to Docker Hub
if: ${{ inputs.dry_run == false && steps.docker-login.outputs.docker-login == 'true' }}
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: acryldata
password: ${{ secrets.DOCSBASE_DH_TOKEN }}
- name: Push
if: ${{ inputs.dry_run == false && steps.docker-login.outputs.docker-login == 'true' }}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 583d04e: dropped the github.event_name check entirely and gated on inputs.dry_run alone, plus a new "Check whether docker login is possible" step that verifies DOCSBASE_DH_TOKEN is actually set before attempting login/push.

} >> "$GITHUB_STEP_SUMMARY"

- name: Dry-run notice
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.dry_run == true }}

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.

Same root cause as the login/push guards above — github.event_name is the caller's event, so this never fires either and a dry run prints nothing.

Suggested change
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.dry_run == true }}
if: ${{ inputs.dry_run == true }}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Same fix as above — this step now just checks inputs.dry_run == true.


assemble-and-publish:
needs: generate
if: ${{ !cancelled() }} # publish whatever versions succeeded, even if some legs failed

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.

!cancelled() ships a corpus with holes under a tag that claims a contiguous range.

With fail-fast: false (L78) a failed leg uploads no artifact but this job still runs, and floor/ceiling are computed from only the surviving trees. So if 1.6.0.5–1.6.0.8 fail in a 12-version backfill, the image still publishes as 1.6.0.1-1.6.0.10 and overwrites :latest, with org.acryl.docsbase.floor/ceiling labels asserting the whole span. A consumer resolving the range and reading /1.6.0.6/schemas/ hits a missing directory at runtime — and the previous good :latest is already gone.

Simplest fix is to not publish an incomplete corpus:

Suggested change
if: ${{ !cancelled() }} # publish whatever versions succeeded, even if some legs failed
if: ${{ success() }} # a failed leg means a hole in the range — don't publish a partial corpus

If you do want partial publishes, then the tag and labels shouldn't imply density — carry the explicit version list instead of <floor>-<ceiling>.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — changed to success() per your suggestion.

Comment on lines +115 to +118
cp docs/generated/ingestion/config_schemas/*_config.json "out/$V/schemas/" 2>/dev/null || \
echo "::warning::no config_schemas produced for $V"
cp -R metadata-ingestion/docs/sources/. "out/$V/docs/" 2>/dev/null || \
echo "::warning::no docs/sources for $V"

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.

These two copies fail soft, so a version can be counted as covered with no schemas/ at all.

2>/dev/null || echo "::warning::..." defeats set -euo pipefail on L112. If docGen runs but emits no *_config.json, the leg still uploads an artifact containing only docs/, and Compute coverage counts that version in versions_count and in the floor-ceiling tag — so the image advertises coverage for a version whose /<version>/schemas/ doesn't exist, found only at consumption time.

Worse case: both copies fail, and if-no-files-found: warn (L125) uploads nothing, so the version silently disappears from the corpus while the run stays green.

Suggested change
cp docs/generated/ingestion/config_schemas/*_config.json "out/$V/schemas/" 2>/dev/null || \
echo "::warning::no config_schemas produced for $V"
cp -R metadata-ingestion/docs/sources/. "out/$V/docs/" 2>/dev/null || \
echo "::warning::no docs/sources for $V"
shopt -s nullglob
schemas=(docs/generated/ingestion/config_schemas/*_config.json)
if [ ${#schemas[@]} -eq 0 ]; then
echo "::error::no config_schemas produced for $V"
exit 1
fi
cp "${schemas[@]}" "out/$V/schemas/"
cp -R metadata-ingestion/docs/sources/. "out/$V/docs/"

Worth flipping L125 to if-no-files-found: error as well, so a leg that produces nothing fails instead of vanishing.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — the corpus assembly step now fails the leg (exit 1) if config_schemas/ wasn't produced, and if-no-files-found is now error instead of warn.

Comment on lines +65 to +73
run: |
set -euo pipefail
if [ -n "${{ inputs.versions }}" ]; then
M='${{ inputs.versions }}'
else
M="$(python3 metadata-ingestion/scripts/docsbase/list_versions.py --days ${{ inputs.days }})"
fi
echo "matrix=$M" >> "$GITHUB_OUTPUT"
echo "Selected versions: $M"

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.

inputs.* interpolated straight into shell — injectable, in a job that holds the push token.

${{ }} substitutes into the script source before bash parses it, so input text becomes shell syntax. A workflow_dispatch with days set to 90; curl -X POST -d "$DOCSBASE_DH_TOKEN" https://… expands on L70 into a second command, in a workflow that secrets: inherit supplies the Docker Hub token to. Less dramatically, a single quote or newline in versions breaks the M='...' assignment on L68 and the $GITHUB_OUTPUT write on L72, giving an opaque fromJson failure downstream.

Passing through the environment means the value can never be re-parsed as code:

Suggested change
run: |
set -euo pipefail
if [ -n "${{ inputs.versions }}" ]; then
M='${{ inputs.versions }}'
else
M="$(python3 metadata-ingestion/scripts/docsbase/list_versions.py --days ${{ inputs.days }})"
fi
echo "matrix=$M" >> "$GITHUB_OUTPUT"
echo "Selected versions: $M"
env:
VERSIONS: ${{ inputs.versions }}
DAYS: ${{ inputs.days }}
run: |
set -euo pipefail
if [ -n "$VERSIONS" ]; then
M="$VERSIONS"
else
M="$(python3 metadata-ingestion/scripts/docsbase/list_versions.py --days "$DAYS")"
fi
echo "matrix=$M" >> "$GITHUB_OUTPUT"
echo "Selected versions: $M"

inputs.image_tag at L170-171 needs the same treatment.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — days/versions/image_tag are now passed via env: instead of interpolated into the script body. Applied the same treatment to matrix.version, which flows from the same versions input and had the same exposure.

# The `push:` trigger on the dev branch is for validation and should be removed
# before merge — steady state is workflow_dispatch (manual/backfill) plus
# workflow_call from publish-pypi-release.yml (auto-refresh on each final release).
name: connector-docsbase

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.

No concurrency group on a workflow that writes a mutable :latest tag.

This repo has published three four-part releases in one day (v1.6.0.12/13/14 around 2026-07-09). Two release: published events start two runs whose generate legs take very different times — each does a full docGen including a fresh uv pip install .[docs] — and both Push steps write …docsbase:latest. Last to finish wins, so :latest can end up serving 1.6.0.12 while 1.6.0.14 is already on PyPI, with no error anywhere.

cancel-in-progress: false rather than true, since each run is a real corpus build you don't want to lose:

Suggested change
name: connector-docsbase
name: connector-docsbase
concurrency:
# Runs all write the mutable :latest tag — serialize them.
group: connector-docsbase
cancel-in-progress: false

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added a concurrency group (cancel-in-progress: false) exactly as suggested.

Comment on lines +6 to +10
# NOTE (scaffold): action refs use version tags for readability; pin to SHAs to
# match repo policy before merge. JDK/Python versions mirror documentation.yml.
# The `push:` trigger on the dev branch is for validation and should be removed
# before merge — steady state is workflow_dispatch (manual/backfill) plus
# workflow_call from publish-pypi-release.yml (auto-refresh on each final release).

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.

This is the "pin before merge" note, and the refs are still floating — flagging so it doesn't ride along into master. A retagged upstream release would run unreviewed code in a job holding the Docker Hub token and every inherited secret.

The SHAs documentation.yml (which the header says this mirrors) currently pins, if useful:

action pinned ref
actions/checkout de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
actions/setup-java be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
actions/setup-python a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
actions/upload-artifact bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
actions/download-artifact 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
docker/login-action b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0

Note those are newer majors than the @v4/@v5/@v3 used here, so pinning is also a version bump — worth a quick check that upload-artifact v7 / download-artifact v8 behave the same for this matrix pattern.

The second half of this note (push: trigger is for validation, remove before merge) is also still outstanding.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Pinned all action refs to the SHAs you listed, including bumping docker/login-action to v4.0.0.

Comment on lines +26 to +27
# docsbase only wants final X.Y.Z.W releases, not rc/post/dev suffixed tags.
if [[ "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then

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.

This regex excludes DataHub's real three-segment releases.

v1.7.0, v1.6.0, v1.5.0 and v1.0.0 all exist as release tags — minor and major bumps ship as X.Y.Z by design. For tag v1.7.0, TAG="1.7.0" fails this match, is_final_release=false, and refresh_docsbase is skipped entirely.

The same four-part pattern is duplicated in metadata-ingestion/scripts/docsbase/list_versions.py:16 (_FOUR_PART), so those versions are dropped from manual/backfill matrices too — they can't enter the corpus by any route, even though :latest's range label spans across them (e.g. 1.6.0.17-1.7.0.3).

The comment says the intent is "final releases, not rc/post/dev suffixed tags", which is really "three or four segments, no suffix":

Suggested change
# docsbase only wants final X.Y.Z.W releases, not rc/post/dev suffixed tags.
if [[ "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
# docsbase wants final X.Y.Z and X.Y.Z.W releases, not rc/post/dev suffixed tags.
if [[ "$TAG" =~ ^[0-9]+(\.[0-9]+){2,3}$ ]]; then

list_versions.py:16 needs the matching change: re.compile(r"^\d+(\.\d+){2,3}$").

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed everywhere this pattern appeared: list_versions.py's _FINAL_RELEASE, the is_final_release check here, and the coverage/prune regex in connector-docsbase.yml (deduped there into one DOCSBASE_VERSION_RE workflow-level env var) — all now ^[0-9]+(\.[0-9]+){2,3}$.

needs: [setup, push_to_pypi]
if: ${{ needs.setup.outputs.is_final_release == 'true' }}
uses: ./.github/workflows/connector-docsbase.yml
secrets: inherit

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.

secrets: inherit is broader than this needs.

The callee declares only DOCSBASE_DH_TOKEN, but inherit hands it the whole set — including TWINE_PASSWORD — while it checks out arbitrary release tags and runs ./gradlew :metadata-ingestion:docGen, i.e. executes that tag's build scripts.

Suggested change
secrets: inherit
secrets:
DOCSBASE_DH_TOKEN: ${{ secrets.DOCSBASE_DH_TOKEN }}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Narrowed to just DOCSBASE_DH_TOKEN.

uses: ./.github/workflows/connector-docsbase.yml
secrets: inherit
with:
versions: '["${{ needs.setup.outputs.tag }}"]'

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.

Passing one version rebuilds :latest from just that version — is that intended?

The callee builds the image FROM scratch out of only the artifacts this run produced, and nothing merges in the previously published corpus. So on v1.7.0.4 the matrix has one entry, floor == ceiling, and it publishes :1.7.0.4-1.7.0.4 plus :latest containing exactly one version. Anything resolving :latest for 1.7.0.3 or earlier then gets a missing path, and the prior multi-version :latest is only recoverable by re-running a backfill.

I might be misreading the intended lifecycle. If :latest is meant to be single-version, then the <floor>-<ceiling> range tag and the sliding-window framing read as promising otherwise. If it's meant to accumulate, this needs to either pull the previous corpus forward before assembling, or pass the full window (days:) instead of the one new tag.

No suggestion here since it depends which way you want it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Went with accumulate: assemble-and-publish now pulls :latest forward, merges in this run's freshly generated versions (which win over stale copies), then prunes anything outside the days retention window — unioned with this run's own versions, so an explicit backfill of an old version isn't immediately pruned in the same run that generated it. Verified live, not just in theory: a single-version dry run correctly showed the merge+prune behavior, then I ran a real (non-dry) full backfill that republished :latest with clean coverage 1.6.0..1.7.0.4 (21 versions, digest sha256:c6b390c54e5f4583ec2f5f295708e74b73ba3fdba44a13800215949f5b0551f5).

shirshanka added a commit that referenced this pull request Aug 15, 2026
…efreshed on release

For each acryl-datahub final release (X.Y.Z or X.Y.Z.W), checks out the tag,
runs docGen (config schemas) and captures metadata-ingestion/docs/sources,
then assembles a single versioned corpus image (/<version>/{docs,schemas}
layout) published as acryldata/datahub-ingestion-docsbase, tagged by the
<floor>-<ceiling> version range it covers plus a moving :latest. Coverage is
recorded both in an in-image CORPUS_MANIFEST.json and as OCI labels so a
consumer can inspect what a given digest covers before pinning it.

Adds metadata-ingestion/scripts/docsbase/list_versions.py, which queries
PyPI for final acryl-datahub releases within a lookback window (used to
build the version matrix for workflow_dispatch backfills).

Wires connector-docsbase.yml up as a workflow_call target from
publish-pypi-release.yml: once a release's push_to_pypi job succeeds, and
only for a genuine final X.Y.Z / X.Y.Z.W tag, it re-runs the docsbase build
for just that version and publishes it (non-dry-run) so the image stays
current without a manual trigger. workflow_dispatch remains available for
manual/backfill runs, with a dry_run input that builds without pushing.

Addresses review from treff7es on PR #402:
- Fix: github.event_name is the CALLER's event inside a workflow_call, never
  'workflow_call' — the login/push/dry-run-notice guards never fired on the
  automated path. Drop the event_name check; gate on inputs.dry_run alone,
  plus a new step that checks DOCSBASE_DH_TOKEN is actually set before
  attempting login/push.
- Fix: the four-part-only version regex (in both list_versions.py and
  publish-pypi-release.yml's is_final_release check, plus the coverage
  computation in this file) excluded real three-part DataHub releases
  (v1.7.0, v1.6.0, ...) from ever entering the corpus. Now accepts X.Y.Z or
  X.Y.Z.W.
- Fix: assemble-and-publish ran on !cancelled(), so a failed matrix leg could
  still publish a corpus with a hole in it under a range tag that claims
  contiguous coverage. Changed to success().
- Fix: the per-version corpus assembly step swallowed a missing
  config_schemas/ or docs/sources with `2>/dev/null || warning`, so a version
  could be counted as covered with nothing in it. Now fails the leg loudly.
- Fix: inputs.days/versions/image_tag were interpolated directly into shell
  script source instead of passed via env, in a job that holds the Docker
  Hub token. Same treatment applied to matrix.version, which flows from the
  same versions input.
- Fix: no concurrency group meant two releases landing close together could
  race on the mutable :latest tag. Added one (cancel-in-progress: false, so
  real corpus builds aren't lost).
- Fix: secrets: inherit handed the reusable workflow the whole secret set
  (including TWINE_PASSWORD) when it only needs DOCSBASE_DH_TOKEN. Narrowed
  to just that secret.
- Pinned all action refs to the SHAs already used elsewhere in this repo
  (documentation.yml), including a docker/login-action v3 -> v4.0.0 bump.

Not addressed here (open design question, tracked separately): whether
:latest should accumulate versions across runs or reflect only the versions
passed to a given run — currently a single-version workflow_call run
rebuilds :latest from just that one version.
@shirshanka
shirshanka force-pushed the ci-connector-docsbase branch from 45af722 to abefd05 Compare August 15, 2026 18:42
shirshanka added a commit that referenced this pull request Aug 16, 2026
…efreshed on release

For each acryl-datahub final release (X.Y.Z or X.Y.Z.W), checks out the tag,
runs docGen (config schemas) and captures metadata-ingestion/docs/sources,
then assembles a single versioned corpus image (/<version>/{docs,schemas}
layout) published as acryldata/datahub-ingestion-docsbase, tagged by the
<floor>-<ceiling> version range it covers plus a moving :latest. Coverage is
recorded both in an in-image CORPUS_MANIFEST.json and as OCI labels so a
consumer can inspect what a given digest covers before pinning it.

:latest is a sliding window, not a per-run snapshot: assemble-and-publish
pulls it forward, merges in this run's freshly generated versions (which
win over stale copies), then prunes anything outside the `days` retention
window (unioned with this run's own versions, so a one-off backfill of an
old version isn't immediately pruned in the same run that generated it).
This is what makes a single-version auto-refresh safe to publish as :latest
without regressing previously-published versions.

Adds metadata-ingestion/scripts/docsbase/list_versions.py, which queries
PyPI for final acryl-datahub releases within a lookback window (used both
to build the version matrix for workflow_dispatch backfills and to compute
the retention window every run).

Wires connector-docsbase.yml up as a workflow_call target from
publish-pypi-release.yml: once a release's push_to_pypi job succeeds, and
only for a genuine final X.Y.Z / X.Y.Z.W tag, it re-runs the docsbase build
for just that version and publishes it (non-dry-run) so the image stays
current without a manual trigger. workflow_dispatch remains available for
manual/backfill runs, with a dry_run input that builds without pushing.

Addresses review from treff7es on PR #402:
- Fix: github.event_name is the CALLER's event inside a workflow_call, never
  'workflow_call' — the login/push/dry-run-notice guards never fired on the
  automated path. Drop the event_name check; gate on inputs.dry_run alone,
  plus a new step that checks DOCSBASE_DH_TOKEN is actually set before
  attempting login/push.
- Fix: the four-part-only version regex (in list_versions.py,
  publish-pypi-release.yml's is_final_release check, and the coverage
  computation in this file) excluded real three-part DataHub releases
  (v1.7.0, v1.6.0, ...) from ever entering the corpus. Now accepts X.Y.Z or
  X.Y.Z.W everywhere, via one shared DOCSBASE_VERSION_RE within this file.
- Fix: assemble-and-publish ran on !cancelled(), so a failed matrix leg could
  still publish a corpus with a hole in it under a range tag that claims
  contiguous coverage. Changed to success().
- Fix: the per-version corpus assembly step swallowed a missing
  config_schemas/ or docs/sources with `2>/dev/null || warning`, so a version
  could be counted as covered with nothing in it. Now fails the leg loudly.
- Fix: inputs.days/versions/image_tag were interpolated directly into shell
  script source instead of passed via env, in a job that holds the Docker
  Hub token. Same treatment applied to matrix.version, which flows from the
  same versions input.
- Fix: no concurrency group meant two releases landing close together could
  race on the mutable :latest tag. Added one (cancel-in-progress: false, so
  real corpus builds aren't lost).
- Fix: secrets: inherit handed the reusable workflow the whole secret set
  (including TWINE_PASSWORD) when it only needs DOCSBASE_DH_TOKEN. Narrowed
  to just that secret.
- Fixed (this round): :latest getting rebuilt from only the versions passed
  to a given run instead of accumulating — see the accumulate/prune
  description above.
- Pinned all action refs to the SHAs already used elsewhere in this repo
  (documentation.yml), including a docker/login-action v3 -> v4.0.0 bump.
@shirshanka
shirshanka force-pushed the ci-connector-docsbase branch from abefd05 to a1e13af Compare August 16, 2026 00:32
…efreshed on release

For each acryl-datahub final release (X.Y.Z or X.Y.Z.W), checks out the tag,
runs docGen (config schemas) and captures metadata-ingestion/docs/sources,
then assembles a single versioned corpus image (/<version>/{docs,schemas}
layout) published as acryldata/datahub-ingestion-docsbase, tagged by the
<floor>-<ceiling> version range it covers plus a moving :latest. Coverage is
recorded both in an in-image CORPUS_MANIFEST.json and as OCI labels so a
consumer can inspect what a given digest covers before pinning it.

:latest is a sliding window, not a per-run snapshot: assemble-and-publish
pulls it forward, merges in this run's freshly generated versions (which
win over stale copies), then prunes anything outside the `days` retention
window (unioned with this run's own versions, so a one-off backfill of an
old version isn't immediately pruned in the same run that generated it).
This is what makes a single-version auto-refresh safe to publish as :latest
without regressing previously-published versions.

Adds metadata-ingestion/scripts/docsbase/list_versions.py, which queries
PyPI for final acryl-datahub releases within a lookback window (used both
to build the version matrix for workflow_dispatch backfills and to compute
the retention window every run).

Wires connector-docsbase.yml up as a workflow_call target from
publish-pypi-release.yml: once a release's push_to_pypi job succeeds, and
only for a genuine final X.Y.Z / X.Y.Z.W tag, it re-runs the docsbase build
for just that version and publishes it (non-dry-run) so the image stays
current without a manual trigger. workflow_dispatch remains available for
manual/backfill runs, with a dry_run input that builds without pushing.

Addresses review from treff7es on PR #402:
- Fix: github.event_name is the CALLER's event inside a workflow_call, never
  'workflow_call' — the login/push/dry-run-notice guards never fired on the
  automated path. Drop the event_name check; gate on inputs.dry_run alone,
  plus a new step that checks DOCSBASE_DH_TOKEN is actually set before
  attempting login/push.
- Fix: the four-part-only version regex (in list_versions.py,
  publish-pypi-release.yml's is_final_release check, and the coverage
  computation in this file) excluded real three-part DataHub releases
  (v1.7.0, v1.6.0, ...) from ever entering the corpus. Now accepts X.Y.Z or
  X.Y.Z.W everywhere, via one shared DOCSBASE_VERSION_RE within this file.
- Fix: assemble-and-publish ran on !cancelled(), so a failed matrix leg could
  still publish a corpus with a hole in it under a range tag that claims
  contiguous coverage. Changed to success().
- Fix: the per-version corpus assembly step swallowed a missing
  config_schemas/ or docs/sources with `2>/dev/null || warning`, so a version
  could be counted as covered with nothing in it. Now fails the leg loudly.
- Fix: inputs.days/versions/image_tag were interpolated directly into shell
  script source instead of passed via env, in a job that holds the Docker
  Hub token. Same treatment applied to matrix.version, which flows from the
  same versions input.
- Fix: no concurrency group meant two releases landing close together could
  race on the mutable :latest tag. Added one (cancel-in-progress: false, so
  real corpus builds aren't lost).
- Fix: secrets: inherit handed the reusable workflow the whole secret set
  (including TWINE_PASSWORD) when it only needs DOCSBASE_DH_TOKEN. Narrowed
  to just that secret.
- Fixed (this round): :latest getting rebuilt from only the versions passed
  to a given run instead of accumulating — see the accumulate/prune
  description above.
- Pinned all action refs to the SHAs already used elsewhere in this repo
  (documentation.yml), including a docker/login-action v3 -> v4.0.0 bump.
@shirshanka
shirshanka force-pushed the ci-connector-docsbase branch from a1e13af to 583d04e Compare August 16, 2026 00:38
@shirshanka

Copy link
Copy Markdown
Author

@treff7es thanks for the thorough review — all findings addressed (replied inline on each), summary here:

  • github.event_name guard bug (the actual blocker) — dropped it, gated on inputs.dry_run alone, added an explicit DOCSBASE_DH_TOKEN != '' check step so login/push safely no-op if the secret isn't set.
  • Three-part release regex bug — fixed in all three places (list_versions.py, is_final_release in publish-pypi-release.yml, and the coverage/prune regex here), deduped the latter into one DOCSBASE_VERSION_RE env var.
  • !cancelled()success() so a failed leg no longer risks publishing a corpus with a hole under a range tag that claims contiguous coverage.
  • Fail-soft corpus assembly → now fails the leg loudly (exit 1) on a missing config_schemas/, and if-no-files-found: error.
  • Shell injection → inputs.days/versions/image_tag (and matrix.version, same exposure) now flow through env: instead of being interpolated into script source.
  • Added the concurrency group.
  • Narrowed secrets: inherit to just DOCSBASE_DH_TOKEN.
  • Pinned all action refs to the SHAs you listed (including the docker/login-action v3→v4.0.0 bump).

Also resolved the accumulate/prune design question: assemble-and-publish now pulls :latest forward, merges in this run's fresh versions (winning over stale copies), and prunes anything outside the days retention window (unioned with this run's own versions). Verified live end-to-end — dispatched a real (non-dry) full backfill and it republished :latest with clean coverage 1.6.0..1.7.0.4 (21 versions, digest sha256:c6b390c54e5f4583ec2f5f295708e74b73ba3fdba44a13800215949f5b0551f5).

Ready for another look whenever you have a chance.

@shirshanka
shirshanka requested a review from treff7es August 16, 2026 04:04

@treff7es treff7es 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.

Approving — round 1 all looks properly addressed, and I verified the result rather than taking it on trust: the published :latest really does carry 21 versions, 1.6.01.7.0.4, built from 583d04eb, with the layout and labels the manifest claims.

Everything below is about the new accumulate/prune code that came in as the fix for my :latest-gets-clobbered comment. The design is right, and the commenting is genuinely good — the github.event_name note in particular is the kind of thing that saves the next person an afternoon. My findings all cluster on one theme: the read-modify-write cycle over :latest has several paths that fail green and silently shrink the published corpus.

Approving rather than blocking because the blast radius is contained: refresh_docsbase runs after push_to_pypi, so it can't break a release; the corpus is fully regenerable with one workflow_dispatch; and nothing consumes the image yet (release-lock.yml / DOCSBASE_DIGEST don't exist in this repo). Worst case is a red job on a successful release, or a stale image.

Three I'd land before merge

  1. Move docker login above the accumulate step, and narrow the "no image yet" classifier (L201). Docker Hub returns the same message for missing and unauthorized — I reproduced it: pull access denied for …, repository does not exist or may require 'docker login'. That matches repository does not exist, so an auth failure reads as "fresh start" and republishes :latest with only this run's versions. The repo is public today so it's latent, but it's the one failure that destroys accumulated state. Moving the login also kills the anonymous-pull rate-limit flake on shared runner IPs.
  2. Fail when dry_run == false and DOCSBASE_DH_TOKEN is empty (L293). Right now login and push both skip, the dry-run notice also skips (dry_run is false), and the job goes green having built an image and thrown it away. Your own test plan still has "confirm the secret is configured" unchecked, which makes this the most likely thing to actually happen on the first release after merge.
  3. Drop the two || trues (L181, L192). Together they're what turn "no artifacts downloaded" into a confident ### docsbase published step summary.

Answering your own open question from round 1

You asked whether upload-artifact v7 / download-artifact v8 behave the same for this matrix pattern. They don't, in one way that matters — see the comment on L161.

Not blocking, but worth deciding before anyone pins this image

The schema-count floor (L139). It's the only failure that produces a corpus which looks complete — right versions, right manifest, quietly missing connectors — and a consumer can't tell "this connector didn't exist at 1.6.0" from "the build dropped it."

One process note: because uses: ./.github/workflows/… resolves at the caller's ref, and the caller runs at the release tag, every fix here only reaches the automated path on releases cut afterwards. So "fix it in a follow-up" has a real delay attached — worth knowing when deciding what rides in this PR.

Verified clean, so no comments on them: list_versions.py passes ruff check, ruff format --check and mypy under the repo's config (it is in mypy scope — setup.cfg excludes only venv|build|dist|examples); sort -V orders 1.6.0.9 < 1.6.0.10 correctly; the bare [ -n "$IMAGE_TAG" ] && … at L261 does not trip set -e; and DOCSBASE_VERSION_RE correctly filters out the dev/etc/proc/sys scaffolding that docker cp drags in — I confirmed the published layer contains only version dirs plus the manifest.

Comment on lines +18 to +22
concurrency:
# Every run writes the mutable :latest tag — serialize so the last one to
# finish is also the last one started, not just the fastest.
group: connector-docsbase
cancel-in-progress: false

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.

cancel-in-progress: false alone doesn't serialize — it drops runs. (Refining my own round-1 suggestion, which was incomplete.)

GitHub's docs: "only one run can be pending in a concurrency group—any additional pending runs cancel the previous one." So with A in progress and B pending, C arriving cancels B. Because each auto-refresh only generates its own version and pulls the rest forward, B's release is then permanently absent until someone backfills by hand. This repo has shipped three four-part releases in a day, and a generate leg is slow, so the window is wide.

There's now a queue key for exactly this:

Suggested change
concurrency:
# Every run writes the mutable :latest tag — serialize so the last one to
# finish is also the last one started, not just the fastest.
group: connector-docsbase
cancel-in-progress: false
concurrency:
# Every run writes the mutable :latest tag — serialize so the last one to
# finish is also the last one started, not just the fastest.
# queue: max is required for true serialization — with the default (single)
# only ONE run may be pending, and a third arrival cancels the queued one,
# permanently dropping that release from the corpus.
group: connector-docsbase
cancel-in-progress: false
queue: max

Comment on lines +118 to +124
# Connector extras build C-extensions (gssapi->krb5, python-ldap->sasl/ldap);
# the bare runner lacks these headers. Mirrors install_deps.sh used by docs CI.
- name: Install system build deps for connector extras
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libkrb5-dev libsasl2-dev libldap2-dev

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.

This doesn't actually mirror install_deps.sh.

The script installs python3-ldap, ldap-utils and unixodbc-dev on top of these three. unixodbc-dev is pyodbc's build dependency, reachable through [docs]base_dev_requirements. I diffed the script at master and at v1.6.0 (the oldest tag in the current corpus) — byte-identical, and it ships in every tag you check out, so calling it stays correct per-version instead of drifting from a hand-copied list. documentation.yml, which the header says this mirrors, just calls it.

Suggested change
# Connector extras build C-extensions (gssapi->krb5, python-ldap->sasl/ldap);
# the bare runner lacks these headers. Mirrors install_deps.sh used by docs CI.
- name: Install system build deps for connector extras
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libkrb5-dev libsasl2-dev libldap2-dev
# Connector extras build C-extensions (gssapi->krb5, python-ldap->sasl/ldap);
# the bare runner lacks these headers. install_deps.sh ships in every tag we
# check out, so it tracks that version's needs instead of drifting from a
# hand-maintained copy here.
- name: Install system build deps for connector extras
run: ./metadata-ingestion/scripts/install_deps.sh

Comment on lines +139 to +142
if [ ${#schemas[@]} -eq 0 ]; then
echo "::error::no config_schemas produced for $V"
exit 1
fi

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.

-eq 0 is far too weak — docGen exits 0 with a partial schema set.

docgen.py wraps per-plugin schema generation in except Exception as e: logger.warning(...) and returns the plugin anyway, so the caller's loaded += 1 branch runs, plugin_metrics.failed is never incremented, and the failed > 0 → sys.exit(1) guard never fires. A connector whose extra failed to install just vanishes with a warning buried in Gradle output.

To calibrate how weak the guard is, I extracted the live image — schemas per version run 100 (1.6.0) → 111 (1.7.0.4), monotonically. A systemic failure yielding 40 sails straight through and overwrites :latest, and the manifest still records that version as fully covered.

Suggested change
if [ ${#schemas[@]} -eq 0 ]; then
echo "::error::no config_schemas produced for $V"
exit 1
fi
# docGen exits 0 even when plugins fail: docgen.py swallows per-plugin
# errors into logger.warning and still counts them as loaded, so a
# systemic problem (missing header, dep rot on an old tag) yields a
# handful of schemas rather than none. Live corpus runs 100 -> 111, so
# this floor only trips on a real collapse.
if [ ${#schemas[@]} -lt "${DOCSBASE_MIN_SCHEMAS:-80}" ]; then
echo "::error::only ${#schemas[@]} config_schemas produced for $V (expected >= ${DOCSBASE_MIN_SCHEMAS:-80})"
echo "::error::check the docGen log for 'Failed to load additional metadata for'"
exit 1
fi

Comparing against the previous version's count would be even better, since the series is monotonic. Narrowing that except Exception in docgen.py is the real fix, but it's outside this diff.

Comment on lines +144 to +145
cp -R metadata-ingestion/docs/sources/. "out/$V/docs/"
echo "schemas: $(find "out/$V/schemas" -mindepth 1 -maxdepth 1 | wc -l) | docs dirs: $(find "out/$V/docs" -mindepth 1 -maxdepth 1 | wc -l)"

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.

Minor asymmetry: zero schemas is now fatal (good), but zero docs isn't. cp -R src/. succeeds on an empty-but-present source dir, the count is printed and never checked, and if-no-files-found: error is satisfied by the schemas alone — so a docs-less version can enter the corpus.

Suggested change
cp -R metadata-ingestion/docs/sources/. "out/$V/docs/"
echo "schemas: $(find "out/$V/schemas" -mindepth 1 -maxdepth 1 | wc -l) | docs dirs: $(find "out/$V/docs" -mindepth 1 -maxdepth 1 | wc -l)"
cp -R metadata-ingestion/docs/sources/. "out/$V/docs/"
docs_n="$(find "out/$V/docs" -mindepth 1 -maxdepth 1 | wc -l)"
[ "$docs_n" -gt 0 ] || { echo "::error::no docs/sources captured for $V"; exit 1; }
echo "schemas: ${#schemas[@]} | docs dirs: $docs_n"

Comment on lines +161 to +165
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: docsbase-*
path: corpus
merge-multiple: true

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.

This answers your round-1 question about v8 behaviour — and the answer is "not the same".

download-artifact has asymmetric failure semantics across its input modes. I checked the pinned source (3e5f45b, v8.0.1):

  • name:throw new Error("Artifact '<name>' not found")
  • artifact-ids:throw new Error("None of the provided artifact IDs were found")
  • pattern: → no throw. It filters the run's artifact list, logs Filtered from X to Y artifacts, and the download block is guarded by if (artifacts.length). It then reports Total of 0 artifact(s) downloaded and exits 0.

This PR uses pattern:. Combined with the || true on L181, zero artifacts becomes a green run that pulls :latest forward, prunes, rebuilds and pushes — byte-identical content, fresh generated_at, and a step summary announcing a successful publish for a release whose docs were never added. It's the most convincing green-but-wrong path in the workflow, because it emits positive evidence of success.

No change needed here; the fix is on L181.

cat > corpus/CORPUS_MANIFEST.json <<EOF
{"floor":"$floor","ceiling":"$ceiling","versions_count":$versions_count,"versions":$versions_json,"source_sha":"$GITHUB_SHA","generated_at":"$generated_at"}
EOF
# Tags: range = true identity (primary), latest = moving pointer.

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.

Small accuracy point: the range tag isn't an identity. Nothing makes <floor>-<ceiling> unique — a re-run, or a backfill of a middle version, reproduces the same floor/ceiling with different content and silently overwrites it. Live tags already show the scheme in use (1.5.0.7-1.6.0.13, 1.6.0-1.7.0.4), and :1.6.0-1.7.0.4 currently resolves to the same digest as :latest.

The "pin the digest" guidance in the step summary is the right mitigation, so this is just the comment overselling it:

Suggested change
# Tags: range = true identity (primary), latest = moving pointer.
# Tags: range is the primary pointer, latest is the moving one. Note the
# range tag is NOT content-addressed — a re-run, or a backfill of a middle
# version, reproduces the same floor-ceiling with different content and
# overwrites it. The digest emitted after push is the only real identity.

Related: nothing verifies the set is gap-free, so a corpus of {1.6.0.9, 1.7.0.3} still publishes as :1.6.0.9-1.7.0.3, which reads as "every release in that interval". CORPUS_MANIFEST.json carries the honest list, but the OCI labels expose only floor/ceiling/count. A contiguous=true|false label would let a consumer tell a range from a range-with-holes.

Comment on lines +274 to +286
- name: Build image
run: |
set -euo pipefail
printf 'FROM scratch\nCOPY corpus/ /\n' > Dockerfile.docsbase
tag_args=()
while IFS= read -r t; do [ -n "$t" ] && tag_args+=(-t "$t"); done <<< "${{ steps.coverage.outputs.tags }}"
docker build -f Dockerfile.docsbase \
--label "org.acryl.docsbase.floor=${{ steps.coverage.outputs.floor }}" \
--label "org.acryl.docsbase.ceiling=${{ steps.coverage.outputs.ceiling }}" \
--label "org.acryl.docsbase.versions_count=${{ steps.coverage.outputs.versions_count }}" \
--label "org.acryl.docsbase.source_sha=$GITHUB_SHA" \
--label "org.acryl.docsbase.generated_at=${{ steps.coverage.outputs.generated_at }}" \
"${tag_args[@]}" .

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.

Two things in this step.

1. inputs.image_tag reaches shell source via ${{ }}. It's unvalidated free text, flows into steps.coverage.outputs.tags (L261-262), and is expanded inside the herestring below. A value containing " terminates it and the rest parses as shell — in a job holding the Docker Hub push token. You already fixed exactly this class in round 1 for days/versions/image_tag; these four sites (L279, plus L310/L313/L328 in Push/Dry-run notice) were missed because the value now arrives via a step output rather than an input.

2. The build context is the whole monorepo. docker build … . tars the entire checkout — plus previous/, a full duplicate of corpus/ — to produce a FROM scratch image that only needs corpus/. It also silently inherits the repo-root .dockerignore, which is maintained for the product images: none of its current patterns match under corpus/, but a future **/docs/-style entry would gut this image with a clean, green build. Worth moving Dockerfile.docsbase into a dedicated dir next to corpus/ and building that as the context.

The env: fix for (1):

Suggested change
- name: Build image
run: |
set -euo pipefail
printf 'FROM scratch\nCOPY corpus/ /\n' > Dockerfile.docsbase
tag_args=()
while IFS= read -r t; do [ -n "$t" ] && tag_args+=(-t "$t"); done <<< "${{ steps.coverage.outputs.tags }}"
docker build -f Dockerfile.docsbase \
--label "org.acryl.docsbase.floor=${{ steps.coverage.outputs.floor }}" \
--label "org.acryl.docsbase.ceiling=${{ steps.coverage.outputs.ceiling }}" \
--label "org.acryl.docsbase.versions_count=${{ steps.coverage.outputs.versions_count }}" \
--label "org.acryl.docsbase.source_sha=$GITHUB_SHA" \
--label "org.acryl.docsbase.generated_at=${{ steps.coverage.outputs.generated_at }}" \
"${tag_args[@]}" .
- name: Build image
env:
TAGS: ${{ steps.coverage.outputs.tags }}
FLOOR: ${{ steps.coverage.outputs.floor }}
CEILING: ${{ steps.coverage.outputs.ceiling }}
VERSIONS_COUNT: ${{ steps.coverage.outputs.versions_count }}
GENERATED_AT: ${{ steps.coverage.outputs.generated_at }}
run: |
set -euo pipefail
printf 'FROM scratch\nCOPY corpus/ /\n' > Dockerfile.docsbase
tag_args=()
while IFS= read -r t; do [ -n "$t" ] && tag_args+=(-t "$t"); done <<< "$TAGS"
docker build -f Dockerfile.docsbase \
--label "org.acryl.docsbase.floor=$FLOOR" \
--label "org.acryl.docsbase.ceiling=$CEILING" \
--label "org.acryl.docsbase.versions_count=$VERSIONS_COUNT" \
--label "org.acryl.docsbase.source_sha=$GITHUB_SHA" \
--label "org.acryl.docsbase.generated_at=$GENERATED_AT" \
"${tag_args[@]}" .

Comment on lines +293 to +297
- name: Check whether docker login is possible
id: docker-login
env:
ENABLE_DOCKER_LOGIN: ${{ secrets.DOCSBASE_DH_TOKEN != '' }}
run: echo "docker-login=$ENABLE_DOCKER_LOGIN" >> "$GITHUB_OUTPUT"

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.

Blocker: with dry_run: false and no secret, this job goes green having published nothing.

The three terminal steps are mutually exclusive and non-exhaustive. When docker-login is 'false': login skips, push skips, and Dry-run notice also skips because dry_run is false. $GITHUB_STEP_SUMMARY is only written inside Push, so it stays empty. The last executed step is Build image, which succeeds — job conclusion: success.

The operator sees a green Refresh connector docsbase image on a workflow whose headline job already passed, and has to notice two grey "skipped" icons to spot it. This can persist across every release indefinitely, with the image simply never moving. Your test plan still lists configuring DOCSBASE_DH_TOKEN as unchecked, so this is the first-release path.

The round-1 guard I suggested was right as a login guard; it just needs to not be silent when publishing was actually requested:

Suggested change
- name: Check whether docker login is possible
id: docker-login
env:
ENABLE_DOCKER_LOGIN: ${{ secrets.DOCSBASE_DH_TOKEN != '' }}
run: echo "docker-login=$ENABLE_DOCKER_LOGIN" >> "$GITHUB_OUTPUT"
- name: Check whether docker login is possible
id: docker-login
env:
ENABLE_DOCKER_LOGIN: ${{ secrets.DOCSBASE_DH_TOKEN != '' }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
set -euo pipefail
if [ "$DRY_RUN" != "true" ] && [ "$ENABLE_DOCKER_LOGIN" != "true" ]; then
echo "::error::dry_run=false but DOCSBASE_DH_TOKEN is unset/empty — refusing to"
echo "::error::build and discard silently. Configure the secret, or pass dry_run=true."
exit 1
fi
echo "docker-login=$ENABLE_DOCKER_LOGIN" >> "$GITHUB_OUTPUT"

Worth flipping the workflow_call secret declaration (L61-63) to required: true at the same time — required: false tells GitHub explicitly not to complain.

Separately: workflow_call's dry_run defaults to true. The current caller passes false correctly, but for a reusable workflow whose entire purpose is publishing, a future caller that omits it gets a silent build-and-discard. The workflow_dispatch default of true is right and should stay.

echo "- range: \`${{ steps.coverage.outputs.range }}\`"
echo "- digest: \`$digest\`"
echo ""
echo "Pin the digest in the consumer (\`release-lock.yml\` → \`DOCSBASE_DIGEST\`)."

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.

release-lock.yml and DOCSBASE_DIGEST don't exist anywhere in this repo — I grepped the whole tree at this ref. If the consumer lives in another repo, worth naming it here so the operator reading this summary knows where to go.

Comment on lines +25 to +32
with urllib.request.urlopen(_PYPI, timeout=30) as r:
data = json.load(r)

cutoff = dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=args.days)
picked: list[tuple[dt.datetime, str]] = []
for version, files in data.get("releases", {}).items():
if not _FINAL_RELEASE.match(version) or not files:
continue

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.

Two robustness gaps, both of which end in the corpus being pruned to nothing (see my comment on connector-docsbase.yml L209).

1. [] is a valid success. .get("releases", {}) turns a PyPI shape change into an empty result, printed on stdout with exit 0. The caller unions that into its retention set and deletes everything outside it. PyPI has repeatedly signalled intent to drop the releases key from /pypi/<project>/json, so this isn't hypothetical.

2. Yanked releases are included. f.get("yanked") is never checked. There are currently 13 yanked acryl-datahub releases; all the non-rc ones are old enough to fall outside any window, so it's latent — but a yanked final release inside the window would be built into the corpus and counted in the retention set.

Suggested change
with urllib.request.urlopen(_PYPI, timeout=30) as r:
data = json.load(r)
cutoff = dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=args.days)
picked: list[tuple[dt.datetime, str]] = []
for version, files in data.get("releases", {}).items():
if not _FINAL_RELEASE.match(version) or not files:
continue
with urllib.request.urlopen(_PYPI, timeout=30) as r:
data = json.load(r)
releases = data.get("releases")
if not isinstance(releases, dict) or not releases:
# Callers union this list into their retention set and prune everything
# outside it, so returning [] silently wipes the published corpus. A
# shape change in PyPI's JSON must fail loudly rather than look empty.
sys.exit("PyPI JSON for acryl-datahub has no usable 'releases' map")
cutoff = dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=args.days)
picked: list[tuple[dt.datetime, str]] = []
for version, files in releases.items():
if not _FINAL_RELEASE.match(version) or not files:
continue
if any(f.get("yanked") for f in files):
continue

Also worth considering, lower priority:

  • Exit non-zero when versions ends up empty (behind an --allow-empty escape hatch), so the caller's matrix can't silently become [].
  • A retry around urlopen. This runs twice per workflow — the second call is in assemble-and-publish, after every generate leg has finished, so a transient PyPI blip there discards tens of job-minutes and ends the run without refreshing :latest.
  • There's a metadata-ingestion/scripts/tests/ directory (test_docgen.py, test_utils.py) and a testScripts Gradle task. The regex and window arithmetic here are load-bearing for the prune step, so a ~30-line test over a canned PyPI payload would be worth it — that's a genuine regression risk, not coverage-chasing.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants