Skip to content

fix(ci): make workflow cron the single Renovate schedule#3

Merged
njhensley merged 2 commits into
mainfrom
ci/renovate-single-schedule
May 4, 2026
Merged

fix(ci): make workflow cron the single Renovate schedule#3
njhensley merged 2 commits into
mainfrom
ci/renovate-single-schedule

Conversation

@njhensley

Copy link
Copy Markdown
Owner

Summary

Removes the second-layer schedule: field from .github/renovate.json5 and aligns the workflow cron to the desired cadence. The workflow cron is now the single source of truth for when Renovate runs.

Motivation / Context

During the practice run on this fork (PR #1), Renovate workflow runs all completed successfully but created zero PRs — every queued bundle stayed in "Awaiting Schedule". Root cause: the two scheduling layers didn't overlap.

Where Previous value Effect
.github/workflows/renovate.yaml cron: "0 9 * * 1" Fires Monday 09:00 UTC
.github/renovate.json5 schedule: ["before 6am every weekday"] Allows PR creation only before 06:00 UTC, Mon–Fri

The cron-triggered run landed three hours after Renovate's window closed, so all PRs were held. workflow_dispatch runs hit the same problem unless the operator was lucky enough to dispatch within the 6-hour weekday window.

For self-hosted Renovate, the config-side schedule: is redundant: Renovate only runs when the workflow fires. Removing the second layer eliminates the alignment trap and makes workflow_dispatch actually useful for ad-hoc runs.

Fixes: silently-empty Renovate runs in PR #1
Related: #1

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • Build/CI/tooling

Component(s) Affected

  • Other: CI / dependency management

Implementation Notes

  • Removed schedule: and timezone: from renovate.json5. Replaced with a comment block explaining the single-source design and pointing at the workflow cron.
  • Workflow cron changed from 0 9 * * 1 (Mon 09:00 UTC) → 0 5 * * 1-5 (Mon–Fri 05:00 UTC). Weekday cadence keeps the CVE-to-PR window tight (self-hosted Renovate can't ingest GitHub vulnerability alerts), and 05:00 UTC means PRs land before standup in most US/EU time zones.
  • Updated .github/RENOVATE.md Architecture and Schedule sections to reflect the new design.

Testing

make lint-renovate                # passes against ghcr.io/renovatebot/renovate:43@sha256:00185c0d...
actionlint .github/workflows/renovate.yaml   # clean

After merge, expected behavior:

  • Next workflow_dispatch will create all 15 queued bundles immediately (no schedule gating, no checkbox dance).
  • Scheduled runs at 05:00 UTC Mon–Fri will create any new bundles Renovate finds.

Risk Assessment

  • Low — Config-only change. The only behavioral change is "PRs that were silently held now actually get created" — which is the intended state. No new auto-merge behavior; no new scope.

Rollout notes: N/A. Effect is immediate on next Renovate run.

Checklist

  • Linter passes (make lint-renovate, actionlint)
  • I did not skip/disable tests to make CI green
  • I updated docs (.github/RENOVATE.md Architecture + Schedule sections)
  • Changes follow existing patterns in the codebase
  • Commits are cryptographically signed (git commit -S)
  • Tests pass locally — N/A, no Go code changes
  • I added/updated tests for new functionality — N/A, no Go code changes

The previous setup had two scheduling layers that didn't overlap: the
workflow cron fired Mondays at 09:00 UTC, while renovate.json5's
`schedule: ["before 6am every weekday"]` only created PRs before 06:00
UTC. Every cron-triggered run landed three hours after the window
closed and Renovate held all PRs in "Awaiting Schedule" — including the
manually-dispatched runs we used during the soft-launch exercise.

For self-hosted Renovate, the config-side `schedule:` field is
redundant: Renovate only runs when the workflow fires, so the workflow
cron already controls cadence. Removing the second layer:

- Eliminates the alignment trap (the failure mode is silent — runs
  "succeed" while creating zero PRs).
- Makes `workflow_dispatch` actually useful — manual triggers now
  create PRs immediately, regardless of clock time.
- Removes the dependency on Dependency-Dashboard checkbox ticking to
  bypass the schedule for ad-hoc runs.

Cron moved from `0 9 * * 1` (Mon 09:00) to `0 5 * * 1-5` (Mon-Fri
05:00) so PRs land before standup. RENOVATE.md updated to document
the single-source-of-truth design and call out the rationale.
Adds `minimumReleaseAge: "3 days"` globally and raises it to 7 days for
the two auto-merge rules. Defends against malicious-publish ratchet
attacks where a poisoned version goes live for hours then gets yanked
(event-stream, colors.js, node-ipc, etc.). Trade-off: legitimate CVE
patches land 3 days later than they would otherwise; for a project
whose security-relevant deps come entirely from upstream tooling we
don't write ourselves, the marginal protection is worth it.

`internalChecksFilter: "strict"` excludes too-young releases rather
than parking them in the Dependency Dashboard as "pending", keeping
the dashboard signal clean — it shows what's about to land, not
what's blocked behind cooldown.

Auto-merge rules carry the higher 7-day cooldown because those
updates skip human review; the cooldown is the only defense layer
left if an attacker pushes a malicious patch.
@github-actions github-actions Bot added size/M and removed size/S labels May 4, 2026
@njhensley njhensley merged commit 2d812b1 into main May 4, 2026
21 checks passed
njhensley added a commit that referenced this pull request May 4, 2026
The `actions/checkout` step was modeled on NVIDIA/gpu-operator's
renovate workflow, but our setup deliberately does not pass
`configurationFile:` (to avoid customManager doubling — see PR #3).
Without that input, the renovatebot/github-action clones the repo
itself inside Docker and has no need for an outer working tree.

Observed failure: every Renovate run since the practice exercise
started would create the first eligible branch (e.g.
`renovate/build-tools`) and then abort 0.3s later with
"Repository has changed during renovation" — even though no commit
to main happened during the run window. The credentials and working
tree planted at /home/runner/work by `actions/checkout` are the
suspect: Renovate's container-internal git operations and the
runner-side git state cross-contaminate, and Renovate misinterprets
its own branch push as a base-branch advance.

Removing the step removes the cross-contamination surface. The action
documents that checkout is not required when using its default
internal-clone mode.
njhensley added a commit that referenced this pull request May 13, 2026
Audit findings #2, #3, #4 against generated evidence bundles:
predicate.validatorImages was always null, predicate.validatorCatalogVersion
was always empty, and the pointer's signer block emitted zero-valued
identity/issuer + rekorLogIndex:0 for unsigned bundles — making a real
Rekor log index 0 indistinguishable from 'no Rekor entry' and an
unsigned bundle indistinguishable from one whose signer fields failed
to extract.

Changes:

  - emitRecipeEvidence loads the validator catalog once and feeds it
    to both the BOM (chart refs + validator images) and the
    predicate's ValidatorCatalogVersion + ValidatorImages fields.
    Shared dedupValidatorImages helper collapses the catalog's
    one-entry-per-check duplication (deployment/perf/conformance all
    share images).

  - PointerAttestation.Signer becomes *PointerSigner, omitempty.
    Unsigned bundles emit the attestation entry with no signer block
    at all; signed bundles always carry non-nil Signer with populated
    Identity and Issuer.

  - PointerSigner.RekorLogIndex becomes *int64, omitempty. nil = no
    Rekor entry created (--no-rekor signing path); non-nil = real
    Rekor log index, even when zero (Rekor's first-ever entry occupies
    index 0 and that's a legitimate position a consumer should be
    able to verify).

  - signAndPushBundle's no-push path returns signPushOutcome{} rather
    than signPushOutcome{Sign: &SignResult{}}, matching its existing
    'All fields are nil when --push is absent' comment and letting
    buildPointerInputs do the right nil-check.

Test coverage:

  - pkg/evidence/attestation/pointer_test.go: unsigned bundles MUST
    omit the signer YAML block; signed-without-rekor bundles MUST
    omit rekorLogIndex. Both assertions check rendered YAML, not just
    the struct.

  - pkg/cli/validate_evidence_test.go (new): catalogVersion,
    dedupValidatorImages, validatorImagesForPredicate helpers; and
    buildPointerInputs covers the three outcome shapes (unsigned,
    signed-with-rekor, signed-without-rekor).

Notes:

  - ValidatorImages.Digest remains blank in this PR. The catalog
    records image refs by tag, not by digest; resolving each ref to a
    digest needs a registry round-trip per image, which validate's
    hot path avoids. Audit item #10 (BOM image digests) tracks the
    longer-term resolver path; the same resolver will populate this
    field when it lands.
njhensley added a commit that referenced this pull request May 13, 2026
Audit findings #2, #3, #4 against generated evidence bundles:
predicate.validatorImages was always null, predicate.validatorCatalogVersion
was always empty, and the pointer's signer block emitted zero-valued
identity/issuer + rekorLogIndex:0 for unsigned bundles — making a real
Rekor log index 0 indistinguishable from 'no Rekor entry' and an
unsigned bundle indistinguishable from one whose signer fields failed
to extract.

Changes:

  - emitRecipeEvidence loads the validator catalog once and feeds it
    to both the BOM (chart refs + validator images) and the
    predicate's ValidatorCatalogVersion + ValidatorImages fields.
    Shared dedupValidatorImages helper collapses the catalog's
    one-entry-per-check duplication (deployment/perf/conformance all
    share images).

  - PointerAttestation.Signer becomes *PointerSigner, omitempty.
    Unsigned bundles emit the attestation entry with no signer block
    at all; signed bundles always carry non-nil Signer with populated
    Identity and Issuer.

  - PointerSigner.RekorLogIndex becomes *int64, omitempty. nil = no
    Rekor entry created (--no-rekor signing path); non-nil = real
    Rekor log index, even when zero (Rekor's first-ever entry occupies
    index 0 and that's a legitimate position a consumer should be
    able to verify).

  - signAndPushBundle's no-push path returns signPushOutcome{} rather
    than signPushOutcome{Sign: &SignResult{}}, matching its existing
    'All fields are nil when --push is absent' comment and letting
    buildPointerInputs do the right nil-check.

Test coverage:

  - pkg/evidence/attestation/pointer_test.go: unsigned bundles MUST
    omit the signer YAML block; signed-without-rekor bundles MUST
    omit rekorLogIndex. Both assertions check rendered YAML, not just
    the struct.

  - pkg/cli/validate_evidence_test.go (new): catalogVersion,
    dedupValidatorImages, validatorImagesForPredicate helpers; and
    buildPointerInputs covers the three outcome shapes (unsigned,
    signed-with-rekor, signed-without-rekor).

Notes:

  - ValidatorImages.Digest remains blank in this PR. The catalog
    records image refs by tag, not by digest; resolving each ref to a
    digest needs a registry round-trip per image, which validate's
    hot path avoids. Audit item #10 (BOM image digests) tracks the
    longer-term resolver path; the same resolver will populate this
    field when it lands.
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.

1 participant