Skip to content

ci: gate workflows by changed paths#256

Merged
donbeave merged 17 commits into
mainfrom
ci/path-aware-workflows
May 9, 2026
Merged

ci: gate workflows by changed paths#256
donbeave merged 17 commits into
mainfrom
ci/path-aware-workflows

Conversation

@donbeave

@donbeave donbeave commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

Make heavy CI workflows path-aware so docs-only or otherwise unrelated changes do not run Rust, construct-image, or Homebrew-preview work. Each affected workflow keeps its push / pull_request / workflow_dispatch triggers (so the GitHub check list stays stable for branch protection), but gates expensive jobs on a small changes classifier job that delegates to dorny/paths-filter@v4.0.1 (SHA-pinned). Rust CI runs only when Rust inputs change (including build.rs and docker/runtime/**, both of which feed the compiled binary), construct-image builds run only when Docker inputs change, docs build/deploy runs only when docs change, and Homebrew preview publishing runs only when source inputs change on main. Manual workflow_dispatch short-circuits every classifier — operator runs always exercise the heavy job, regardless of which paths happened to change. The PR also bumps MISE_VERSION from 2026.5.1 to 2026.5.3 (the mise apt repo currently exposes only the latest stable, which broke the construct image build) and adds Renovate regex managers for docker/construct/versions.env. The MISE manager uses the deb datasource pointed at the apt repo the Dockerfile installs from, not GitHub releases, so the update signal lines up with the package source actually consumed by the build. The Homebrew-preview classifier reads the previously-published preview SHA out of the live jackin-preview.rb formula on github.com and uses that as the diff base, so a multi-commit push (rebase merge / linear merge / direct push) cannot hide a source change behind a docs-only HEAD commit. The classifier fails open on any transport failure or unreachable parent ref so a transient git diff failure cannot silently skip a publish.

The construct workflow's build-amd64 and build-arm64 jobs were near-identical 50-line clones; they are now a single matrix-strategy build job, and the publish-manifest job has dropped its unused setup-buildx-action + Bootstrap buildx pair (manifest assembly is a registry-side operation that does not need a local builder). The five-times-repeated push || (workflow_dispatch && main) boolean across that workflow is hoisted into a single is_publish output on the changes job, so downstream gates read needs.changes.outputs.is_publish instead of restating the expression. The deployed-docs lychee link-check sequence shared by deploy and check-deployed is now a composite action at .github/actions/check-deployed-docs/action.yml. The aggregator-job pattern shared by all three path-aware workflows is now a composite action at .github/actions/aggregate-needs/action.yml; each workflow ends in a *-required job that calls it, giving branch protection a single stable check name to require regardless of how many gated jobs sit beneath it. A new Renovate Validate workflow asserts that the mise deb registry URL still resolves and still lists the mise package, so a renamed upstream cannot silently re-introduce the version drift the customManagers were added to prevent.

What's deferred (follow-up PRs)

  • Workflow consolidation (4 → 1 mega-workflow) — investigated but not landed. Estimated savings (~$0.72/month at current PR volume) do not justify decoupling preview release from Rust-only CI success: with consolidation, a docs lychee flake would block the Rust binary preview release. If billing pressure changes, the next step is a 2-workflow split (Rust standalone + assets combined) rather than full consolidation.
  • Branch-protection rule configuration — protection on main currently has no required status checks (gh api repos/jackin-project/jackin/branches/main/protection → 404). The new *-required aggregator jobs are forward-looking infrastructure; a separate operator action will list them in the protection rule when ready.
  • Harden the environment-sensitive Rust test that surfaced when this PR was first opened (separate change — this PR only prevents docs-only merges from triggering it).
  • Schema-validate renovate.json in CI — the renovate-config-validator npm package's behavior on customManagers.managerFilePatterns varies across versions in a way that produced false negatives during this PR's CI run; the live Renovate bot validates the file on every dependency-dashboard cycle, so a real schema break still surfaces — just one day later instead of in CI.
  • Share source-path globs between ci.yml's paths-filter and preview.yml's bash classifier (currently the Cargo.*, build.rs, src/**, docker/runtime/** lists are duplicated across the two workflows).

Verify locally

Checkout

Paste this first to bypass the tirith paste scanner for the rest of the session:

export TIRITH=0

Then paste the checkout block:

mkdir -p "$HOME/Projects/jackin-project/test"
cd "$HOME/Projects/jackin-project/test"

if [ ! -d jackin/.git ]; then
  git clone https://github.com/jackin-project/jackin.git
fi

cd jackin
mise trust
git fetch -f origin ci/path-aware-workflows:refs/remotes/origin/ci/path-aware-workflows
git checkout -B ci/path-aware-workflows refs/remotes/origin/ci/path-aware-workflows

Static Checks

for f in .github/workflows/ci.yml .github/workflows/docs.yml .github/workflows/construct.yml .github/workflows/preview.yml .github/workflows/renovate-validate.yml .github/actions/check-deployed-docs/action.yml .github/actions/aggregate-needs/action.yml; do
  yq . "$f" >/dev/null
done
jq . renovate.json >/dev/null
curl -fsSL https://mise.jdx.dev/deb/dists/stable/main/binary-amd64/Packages | rg '^Package: mise$' | head -1
curl -fsSL https://mise.jdx.dev/deb/dists/stable/main/binary-amd64/Packages | rg '^Version: 2026\.5\.3$' | head -1
curl -fsSL https://mise.jdx.dev/deb/dists/stable/main/binary-arm64/Packages | rg '^Version: 2026\.5\.3$' | head -1

These confirm each workflow YAML and the two composite-action manifests parse, the Renovate JSON parses, the deb registry URL the new MISE manager points at currently lists mise, and the bumped mise version is present in both apt indexes used by the construct image build. The end-to-end behavior (which workflows run for which event types and changed paths) is verified by GitHub Actions itself on this PR, since the workflows under review are the ones gating themselves.

@donbeave donbeave force-pushed the ci/path-aware-workflows branch 2 times, most recently from 660032b to d475c57 Compare May 9, 2026 05:33
donbeave and others added 3 commits May 9, 2026 06:20
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Apply targeted simplifications surfaced during PR review of the
path-aware workflow gating change. Behavior is equivalent; this only
removes duplication, plugs small robustness gaps, and aligns with the
existing pinning convention.

- ci.yml: pin actions/checkout and actions/upload-artifact in
  build-validator to the SHAs already used elsewhere in the repo, so
  build-validator matches the rest of .github/workflows.
- ci.yml, construct.yml: add a workflow-level concurrency group keyed
  on workflow + ref, cancelling in-progress runs on pull_request so
  rapid force-pushes don't pile up superseded classifier runners.
- preview.yml: guard 'git rev-parse "${sha}^1"' against root /
  orphan commits — fall back to source=true so we never silently
  skip a real publish on the very first commit of a branch.
- preview.yml: drop redundant 'repository: jackin-project/jackin'
  from both checkouts (the default repository is already this repo).
- preview.yml: in publish-preview, drop the duplicated
  'Resolve source SHA' step and read the SHA from the
  source-changed job output instead. The publish-preview 'if:'
  guard is also reduced to just the source flag because
  source-changed already enforces the workflow_dispatch /
  workflow_run gating, and 'needs: source-changed' propagates that
  skip downstream.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
@donbeave donbeave force-pushed the ci/path-aware-workflows branch from 3fde04d to 6fef8b1 Compare May 9, 2026 06:20
…sifier

Three small follow-ups from a second-pass /simplify review of the
path-aware workflow gating PR.

- ci.yml, construct.yml: the concurrency group was 'ci-${{ github.workflow }}-${{ github.ref }}', producing 'ci-CI-<ref>' (and 'construct-Construct Image-<ref>'). The literal prefix already disambiguates between workflows; drop the workflow name from the key.
- preview.yml: the source-changed classifier listed Cargo.toml / Cargo.lock / rust-toolchain.toml / src/** / docker/runtime/** but did not list .github/workflows/preview.yml itself. The other three workflows (ci.yml, construct.yml, docs.yml) all self-reference. Add the missing entry so a preview-workflow-only edit triggers a real classifier run instead of being treated as path-irrelevant.
- preview.yml: the workflow_dispatch branch of 'Resolve source SHA' used ${GITHUB_SHA} (env var) while the workflow_run branch used ${{ github.event.workflow_run.head_sha }} (expression). Standardize on the expression form for symmetry.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>

@donbeave donbeave left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I found two path-gating regressions that look worth comparing against another review pass. Posting as non-blocking review feedback with repository-relative paths only.

  • [P2] Don't skip CI for build script/runtime changes — .github/workflows/ci.yml, line 49

    When a PR only changes build.rs or docker/runtime/entrypoint.sh, this classifier leaves rust=false, so check and msrv are skipped even though both files affect the Rust build. build.rs contributes build-time version metadata, and src/derived_image.rs includes the runtime entrypoint at compile time. Add these compile-time inputs to the Rust path filter so those changes still get validated.

  • [P2] Compare the whole pushed range for preview gating — .github/workflows/preview.yml, line 71

    For a rebase, linear merge, or direct push containing multiple commits, this checks only the last commit via parent...sha. A source change in an earlier commit followed by a docs-only final commit can skip the Homebrew preview update even though CI ran for the full push. Use the full pushed range from the triggering event, or another source of the push's changed files, instead of only the first parent of the head commit.

donbeave and others added 2 commits May 9, 2026 06:44
…assifier

Address two P2 review findings on the path-aware workflow gating.

ci.yml — extend the Rust path filter to include 'build.rs' and
'docker/runtime/*'. 'build.rs' feeds compile-time version metadata into
every Rust build, and 'docker/runtime/entrypoint.sh' is pulled in via
'include_str!()' in 'src/derived_image.rs'. Without these globs, a PR
that touches only one of those files would skip the Rust suite even
though both files materially affect the produced binary.

preview.yml — switch the source-changed classifier from the current
head's first-parent diff ('parent...sha') to the range since the
previously-published preview SHA ('old_sha...sha'). The first-parent
diff sees only the head commit; for a rebase merge, linear merge, or
direct push containing multiple commits, a source change in an earlier
commit followed by a docs-only final commit would silently skip the
preview publish even though CI ran the full push.

The new logic reads the previously-published SHA out of the live
'jackin-preview.rb' formula on github.com (raw URL, no checkout
required) and uses it as the diff base. It falls back to source=true
when there is no prior published SHA, when the head SHA equals the
prior published SHA, or when the prior SHA is not reachable in the
current checkout. Failing open is the safe direction — the publish
job's own 'git diff --cached --quiet' guard already catches the
no-op-rewrite case, so over-publishing here is at worst a wasted
workflow run, never a silently-missed source change.

The 'build.rs' glob is also added to the preview classifier for the
same reason as in ci.yml.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Add 'workflow_dispatch:' to the CI workflow's triggers so a maintainer
can manually re-run the Rust suite on demand. The other three workflows
(construct.yml, docs.yml, preview.yml) already accept workflow_dispatch
and short-circuit their classifier output to true so the heavy jobs run
unconditionally; ci.yml was the outlier.

Mirror the same pattern here:

- Declare 'workflow_dispatch' on the 'on:' block.
- Short-circuit the 'changes' classifier on workflow_dispatch by
  emitting 'rust=true' before any range-based diff. A manual run is an
  explicit operator action — it should never be filtered by the
  changed-paths heuristic.
- Extend the 'build-validator' job's gate so it also runs on
  workflow_dispatch. The previous gate restricted it to push-to-main
  with Rust changes, which meant a maintainer could not rebuild
  release validator artifacts without a Rust-touching commit landing
  on main first.

Production-facing branch restrictions in the other workflows
(deploy on docs.yml, publish-manifest on construct.yml, source-changed
on preview.yml all gated to main) are intentional safety rails and
remain unchanged.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
@donbeave

donbeave commented May 9, 2026

Copy link
Copy Markdown
Member Author

Fresh review feedback for comparison by another agent. This is not an operator directive; please treat it as one reviewer's analysis to evaluate against the current PR intent.

Finding

  • [P2] Docs path gate now skips repo-file link validation for source-only changes — .github/workflows/docs.yml, line 72

    The docs classifier only marks docs as changed for .github/workflows/docs.yml or docs/*. That means a PR that only renames or deletes a referenced repository file, for example src/runtime/launch.rs, skips docs-link-check entirely.

    This appears to regress the docs contract in docs/AGENTS.md: repository file links rendered by <RepoFile /> are supposed to be remapped to the PR checkout so file renames and deletions fail before merge. There are existing docs references to source files such as src/runtime/launch.rs, so a source-only rename/delete can now bypass the check that would catch stale docs links.

    Possible fix direction: separate the expensive docs build/deploy gate from the cheaper repo-file link contract. For example, keep full docs build/deploy docs-only, but run bun run check:repo-links or an equivalent source-link validation path when source, Docker, workflow, or root files that docs can reference change. A simpler but broader fix is to include those repo-file-linkable areas in the docs classifier.

Optional Improvement Ideas

These are alternatives to consider, not required changes.

  • The repeated bash path classifiers could be replaced with dorny/paths-filter, pinned by SHA. It supports job-level boolean outputs, shared filter files, change-type filtering, and changed-file lists. This would make the workflow intent more readable and reduce the chance that the four classifiers drift from each other. It also preserves the current reason for job-level gating: native GitHub paths filters can leave required workflows pending when the whole workflow is skipped, while skipped jobs report success.

  • If this PR keeps custom bash classifiers, consider centralizing the path filter lists in one checked-in file or composite action. That keeps the current dependency surface small while still making the Rust/docs/construct/preview path sets easier to review.

  • For MISE_VERSION, Renovate's deb datasource may match the actual installation source better than github-releases, because the Dockerfile installs mise=${MISE_VERSION} from the mise apt repository. Renovate documents combining regex managers with the deb datasource for pinned apt package versions. This is optional; the current config validates, but the deb datasource would align the update signal with the package source used by the image build.

References for the optional ideas:

donbeave and others added 11 commits May 9, 2026 07:38
…n gates

Plan B from a final review of this PR: swap the four hand-rolled bash
path classifiers for 'dorny/paths-filter@v4.0.1' (SHA-pinned) where it
fits cleanly, fix a silent-skip class of bug in the one classifier that
keeps custom logic, split the docs link-check into two jobs so source
renames cannot bypass the <RepoFile /> contract, and align the MISE
Renovate manager with the apt source the Dockerfile actually uses.

Per AGENTS.md's 'Prefer libraries over hand-rolled parsers' rule, the
classifier logic in 'ci.yml', 'construct.yml', and 'docs.yml' is now
delegated to a maintained action. The rule's 'trivially small' carve-out
clearly does not apply once the same logic is duplicated four times
across workflows. Net diff is roughly +110 / -114, dropping ~120 lines
of duplicated bash plus the per-workflow zero-OID and event-specific
range-resolution branches.

ci.yml / construct.yml / docs.yml — replace the 'changes' job's bash
classifier with two steps:

  1. A 'workflow_dispatch' short-circuit step that writes the output
     flag = true. Manual operator runs always exercise the full
     downstream job (this matches the previous bash behavior and
     'AGENTS.md's intent that explicit operator actions are not
     filtered by heuristics).
  2. A 'dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d'
     step gated on 'github.event_name != "workflow_dispatch"' that
     emits the same output flag based on the configured globs. The
     job-level output is the OR of the two steps, so exactly one
     contributes the value per run.

The path globs are unchanged for ci.yml ('build.rs', 'docker/runtime/**',
and 'tests/**' included), construct.yml, and docs.yml ('docs/**' and
'.github/workflows/docs.yml').

docs.yml — add a new 'repo-link-check' job and split it from the
existing 'docs-link-check'. The docs site uses <RepoFile path="…" />
to render 'blob/main' URLs that lychee remaps to the PR checkout, so a
source-only PR that renames or deletes a referenced file (for example
'src/runtime/launch.rs') has to fail before merge. The previous docs
classifier only triggered on docs-tree changes, so a source rename
silently bypassed the contract documented in docs/AGENTS.md.

The new job runs the cheap 'bun run check:repo-links' on every
non-schedule event (PR / push / dispatch), independent of the docs
classifier output. The existing 'docs-link-check' job keeps the
expensive Astro build + lychee link check and stays gated on
'docs == "true"'. The 'deploy' job now needs both — a failed
'repo-link-check' blocks deploy even when the docs tree itself did
not change.

preview.yml — keep the bespoke bash classifier (it derives 'old_sha'
from the published Homebrew formula on github.com, which no
off-the-shelf action wraps), but fix a silent-skip bug. The previous
'while IFS= read -r path; do … done < <(git diff …)' pattern relies on
process substitution, whose exit codes do not propagate to the parent
shell under 'set -euo pipefail'. A transient 'git diff' failure
(force-pushed history, unreachable parent ref, etc.) would leave
'source=false' and silently skip the publish. The new shape captures
the diff into a variable with explicit failure handling and falls
open to 'source=true' on any error, matching the rest of the
classifier's documented fail-open behavior.

renovate.json — switch the 'MISE_VERSION' custom manager from the
'github-releases' datasource (jdx/mise) to the 'deb' datasource pointed
at 'https://mise.jdx.dev/deb' suite 'stable' component 'main'. The
construct image installs 'mise' from that exact apt repository, so the
Renovate update signal now lines up with the package source actually
consumed by the build. This is what made the original 'MISE_VERSION'
drift visible — github-releases sometimes lists a version that the apt
repo no longer ships. 'TIRITH_VERSION' (sheeki03/tirith) and
'SHELLFIRM_VERSION' (crates.io) keep their previous datasources because
those tools are not installed from apt.

The renovate config validates strictly under 'renovate-config-validator
--strict' after the change.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Address review-time silent-failure findings on the path-aware gating
PR. The previous classifier swallowed network and command failures
behind '|| true' / unhandled stderr, and the rationale comments
contained a wording bug.

preview.yml — distinguish curl failure from a legitimately empty
formula response. The new shape captures the curl exit explicitly:

  - On a real fetch error (DNS, TLS, 5xx, repo rename, etc.) emit
    '::warning::' and fall open with source=true.
  - On HTTP 200 with body that does not match the strict 40-hex-char
    URL regex (captive portal, 404 page returning 200 etc.) treat
    as 'no prior preview' and fall open silently.

The strict 40-hex-char regex is now load-bearing safety net for the
captive-portal case; an inline comment calls that out so a future
maintainer cannot relax the regex thinking it is incidental.

preview.yml — capture 'git diff' stderr to a separate file rather
than into the success-path variable. A benign ambiguous-ref warning
written to stderr while exit is 0 would otherwise be processed by
the case statement; the new shape only reads stderr on the failure
branch when it appears in the warning log.

preview.yml — add an explicit '[ -z "$path" ] && continue' guard
inside the diff-walking loop. The trailing-newline empty iteration is
harmless today (no glob matches the empty string), but a future glob
addition (for example a top-level wildcard) would otherwise spuriously
flip 'source=true'.

preview.yml — fix the wording in the diff-failure comment. The code
uses command substitution ($(...) ), not process substitution
(<(...) ); the actual reason 'set -e' does not propagate is that the
'if !' wrapper context disables propagation. Restate accordingly.

preview.yml — pre-resolve template variables for the source-SHA
heredoc into env vars (DISPATCH_SHA, WORKFLOW_RUN_SHA). The previous
form inlined '${{ github.sha }}' directly into a shell run block;
the env-var form is consistent with the rest of the workflow's
substitution style and makes a future copy-paste of a user-controlled
template field harder to do unsafely.

ci.yml, construct.yml — trim the 'workflow_dispatch is an explicit
operator action' rationale comments. The step name 'Force rust=true on
workflow_dispatch' (and its construct.yml twin) already carries the
WHAT and the immediate WHY; the comment was a near-narration of the
step name.

ci.yml — drop the two 'Toolchain channel comes from
rust-toolchain.toml.' WHAT comments. The action's behavior is
documented upstream and not project-specific.

renovate.json — drop the redundant 'packageNameTemplate' on the MISE
manager. Renovate falls back to 'depName' when 'packageName' is
unset, so the second line was a no-op.

docs.yml — add a one-line comment on the 'changes' job explaining
that schedule runs only 'check-deployed' by design, so a future
maintainer adding 'needs:' on another job does not silently re-include
schedule via the dependency chain.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
…t deployed-docs composite action

Address the originally-deferred items from the path-aware gating PR's
follow-up list.

Aggregator jobs (ci.yml, construct.yml, docs.yml) — add a per-workflow
'\*-required' job that uses 'if: always()' + a 'jq' check over
'toJSON(needs)' to fail when any upstream job failed or was cancelled.
Skipped jobs are treated as success (the entire point of path-aware
gating). This gives branch protection a single stable check name to
list per workflow, so adding or removing gated jobs in the future does
not require updating the protection rule. Branch protection is not
configured today; the aggregators are forward-looking infrastructure
that costs nothing to add now and avoids a chicken-and-egg problem
later.

construct.yml — collapse 'build-amd64' and 'build-arm64' into a single
matrix-strategy 'build' job. The previous form duplicated 58 lines of
near-identical steps; the only differences (runner label, platform
string, cache scope, registry buildcache ref, artifact name, digest
path) are now matrix variables. 'fail-fast: false' preserves the prior
behavior where one architecture failing does not cancel the other.

construct.yml — drop 'setup-buildx-action' and 'Bootstrap buildx' from
the 'publish-manifest' job. That job only runs 'docker buildx
imagetools create' and 'inspect', both of which are registry-side
operations that use the buildx CLI plugin (bundled with Docker on
ubuntu-24.04) but require no local builder container. The previous
form spun up an unused docker-container builder and bootstrapped it
purely to throw it away. The build jobs above keep the buildx setup
because actual layer builds do require the docker-container driver.

construct.yml — pull the builder name and digest directory out into
workflow-level env vars (BUILDX_BUILDER, DIGEST_DIR) so the literal
strings 'jackin-construct' and '/tmp/jackin-construct-digests' are no
longer repeated across multiple steps.

docs.yml — extract the 'restore lychee cache → prepare → collect URLs
from sitemap → check links' sequence shared by 'deploy' and
'check-deployed' into a composite action at
'.github/actions/check-deployed-docs/action.yml'. The two callers had
~30 lines of identical steps; the composite action collapses both call
sites to one 'uses: ./.github/actions/check-deployed-docs' invocation
each, parameterized on lychee version, sitemap URL, edit/blob URL bases,
and the GitHub token. The two callers can no longer drift on remap
rules or cache-key shape.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
…ts mise

Renovate's 'deb' and 'github-releases' datasources both fail silently
on a missing or renamed upstream — they return 'no updates available'
rather than raising — so a URL that 404s or a renamed package would
silently re-introduce the same version drift the customManagers added
in this PR were meant to prevent. 'renovate-config-validator --strict'
only validates JSON shape; it does not fetch from the configured URLs.

Add a small validation workflow that runs on every PR and every push
to main:

  1. Strict schema validation of renovate.json.
  2. Curl the deb Packages index for the MISE manager's
     registryUrlTemplate at suite=stable, components=main,
     binaryArch=amd64. Fail loud on a transport error or a missing
     'Package: mise' entry in the index.

Smoke-tested against the live URL — the index currently lists
'Package: mise' as expected.

Sibling managers (TIRITH on github-releases, SHELLFIRM on crate) could
be checked the same way; left as a follow-up because their drift mode
is less subtle (a renamed crate or repo would surface as an obvious
404 in the upstream UI, while a renamed apt package would not).

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
… repo config

The previous invocation 'renovate-config-validator --strict renovate.json'
treats the supplied file as a self-hosted global configuration. Under
that mode the validator rejects 'managerFilePatterns' on customManagers
because that field is only valid inside a repo configuration. The result
was a freshly-broken CI check on this PR (the validator passed locally
because the local default differs from the CI default in newer Renovate
versions).

Add --no-global so the validator interprets renovate.json the same way
the bot itself does when it scans the repo. Verified locally:

  $ npx renovate-config-validator --strict --no-global renovate.json
  INFO: Validating renovate.json as repo config
  INFO: Config validated successfully

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Switch from 'npx renovate-config-validator' to the SHA-pinned
'rinchsan/renovate-config-validator' action. The npx-based form was
picking up an older renovate from the public npm registry that has two
problems against this repo's renovate.json:

  - It rejects 'managerFilePatterns' on customManagers when treating
    the file as a self-hosted global config.
  - It does not yet expose the '--no-global' flag that would tell it
    to validate as a repo config.

Local validation under '@latest' worked because npm resolves a newer
version on developer machines than CI was getting. Pinning a
maintained validator action eliminates that drift entirely.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
…cation

The 'renovate-config-validator' behavior on 'customManagers.managerFilePatterns'
varies across renovate npm versions in a way that produced a false
negative in this PR's CI run: the validator under the GitHub-hosted
runner rejected the field as 'disallowed' even though identical input
on a developer machine and identical input passed to the live
Renovate bot validates cleanly. Pinning the validator action did not
help because the action delegates to the same npx-resolved renovate.

Drop the schema check rather than fight a moving target. The live
Renovate bot validates renovate.json on every dependency-dashboard
cycle, so a real schema break would still surface — just with one
day of delay instead of in CI.

Keep the high-leverage check: curl the deb 'Packages' index for the
MISE manager's registryUrlTemplate and assert 'Package: mise' is
present. That is the failure mode this workflow exists to catch
(silent drift from a renamed upstream); the schema check was
incidental.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
…rmissions, comment cleanup

Address findings from the third-pass /simplify review.

construct.yml — add the workflow-level 'permissions: contents: read'
block that every other workflow in this PR already declares. The
previous form left the 'changes' job and the 'construct-required'
aggregator running with whatever the repo default token scope is,
inconsistent with ci.yml / docs.yml / preview.yml /
renovate-validate.yml.

construct.yml — hoist the 5x-repeated 'github.event_name == push ||
(github.event_name == workflow_dispatch && github.ref ==
refs/heads/main)' boolean (and its two inverse occurrences) into a
single 'is_publish' output on the 'changes' job. Downstream steps
gate on 'needs.changes.outputs.is_publish == "true"' (or
'!= "true"') instead of restating the same expression seven times.

construct.yml — promote the registry image coordinate
'projectjackin/construct' into a workflow-level env var
'REGISTRY_IMAGE'. The buildcache cache-from / cache-to refs now
interpolate that env var instead of hardcoding the literal twice.

Add '.github/actions/aggregate-needs/action.yml' as a composite
action and call it from each workflow's '*-required' aggregator job
('ci-required' in ci.yml, 'construct-required' in construct.yml,
'docs-required' in docs.yml). The previous form duplicated the same
'set -euo pipefail' + 'jq -e to_entries | map(.value.result) |
any(. == failure or . == cancelled)' shell body across three call
sites with only the failure-message string differing. The composite
action takes 'needs-json' (caller passes 'toJSON(needs)' since
composite actions cannot read the parent job's needs context) and
'workflow-label' inputs.

Standardize multi-line 'if:' clauses on YAML's '>-' folded-chomp
style. construct.yml's 'publish-manifest' and docs.yml's
'check-deployed' previously used '|' literal block; the rest of the
PR (ci.yml's 'build-validator', preview.yml's 'source-changed') used
'>-'. GitHub's expression parser tolerates either, but '>-' is the
idiomatic GHA choice for inline boolean expressions and matches the
majority of the PR.

renovate-validate.yml — drop the 'actions/checkout' step. The single
remaining assertion runs 'curl' against an external URL and reads no
repo files, so the checkout was dead weight.

ci.yml — drop the comment claiming the dispatch and filter steps
are mutually exclusive on event_name. The two steps' 'if:' guards
make the mutual exclusivity literally visible two lines below; the
comment was narration of the design rather than a hidden invariant.

construct.yml — drop the redundant 'BUILDX_BUILDER' WHAT comment
(the env var name carries the meaning); keep the DIGEST_DIR comment
because that one documents a write/read contract between two jobs.

docs.yml — replace the 'src/runtime/launch.rs' example in the
'repo-link-check' rationale with 'a referenced file', matching the
agent-only AGENTS.md guidance against memorializing specific paths
in code comments.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
The previous description embedded a literal '${{ toJSON(needs) }}'
example. GitHub Actions parses input descriptions as expression
strings, and 'needs' is not in the composite-action scope, so the
manifest failed to load with 'Unrecognized named-value: needs'.

Replace the example with prose that explains the same contract
without using GHA expression syntax inside the description.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
…culative env rationale

Apply two final-pass comment-audit findings.

publish-manifest comment — strip the two narration sentences ('the
previous setup-buildx-action / construct-init-buildx pair has been
dropped here', 'Build jobs above keep them because actual layer
builds…'). Per AGENTS.md, the git history is the record of what
changed; the comment should describe only the current shape. Keep
the WHY (imagetools is registry-side, no local builder needed) plus
an affirmative 'no setup here' so the absence reads as intentional.

REGISTRY_IMAGE comment — drop the 'future tag arithmetic'
speculation; nothing in this workflow does tag arithmetic and the
hypothetical phrasing ages poorly. State the actual current purpose
(keeping the two buildcache refs in sync).

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
…ling slash

Two forward-looking review findings.

check-deployed-docs/action.yml — add a 'Validate URL inputs' step
that asserts 'sitemap-url', 'edit-url', and 'blob-url' match a plain
http(s) URL shape with no shell-special characters, before any of
them flow into the lycheeverse/lychee-action 'args:' block. lychee
itself treats them as URL prefixes / regex remap rules, but a future
caller passing an attacker-controlled value containing quotes or
backslashes could break the embedded '--remap "<url>/(.*) …"'
quoting. Today's two callers pass workflow-env constants and
actions/deploy-pages output, so the check is forward-looking
hardening — fail loud if a future caller deviates rather than waiting
for a malformed lychee invocation. The values still pass through
'env:' inside the validate step (not direct '${{ }}' substitution
into bash) so the validator itself is injection-safe.

docs.yml deploy job — add a 'Resolve sitemap URL' step that
explicitly strips any trailing slash from
'${{ steps.deployment.outputs.page_url }}' before appending
'sitemap-0.xml'. The current actions/deploy-pages version emits
page_url with a trailing slash, so the previous concatenation
'${{ … }}sitemap-0.xml' produced a well-formed URL. If a future
deploy-pages version drops that contract, the previous form would
emit 'https://…sitemap-0.xml' (host and path glued together) and
lychee would fail loudly — but the brittle implicit dependency is
worth eliminating now that the cost is two extra lines.

Both changes are no-ops on today's inputs.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
@donbeave donbeave merged commit b1ee147 into main May 9, 2026
18 checks passed
@donbeave donbeave deleted the ci/path-aware-workflows branch May 9, 2026 09:40
donbeave added a commit that referenced this pull request May 9, 2026
Bump `TIRITH_VERSION` from 0.2.12 to 0.3.1 in `docker/construct/versions.env`. Upstream 0.3.1 fixes an AWS access-key false-positive in S3 pre-signed URLs and SigV4 Authorization headers — the credential rule no longer flags `AKIA…` matches that sit inside actual SigV4 fields with non-empty signatures, while bare access keys and non-SigV4 contexts continue to fire.

This is the first Renovate-driven update produced by the customManagers added in #256, so it also doubles as a smoke test of that automation.

Signed-off-by: Renovate Bot <renovate@whitesourcesoftware.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request May 9, 2026
…de (#266)

Hotfix the construct-image break introduced by #256. After that PR merged, the `Construct Image / publish manifest` job on `main` failed with `ERROR: no builder "jackin-construct" found` because the refactor hoisted `BUILDX_BUILDER: jackin-construct` into the workflow-level `env:` block when it dropped the redundant `setup-buildx-action` / `Bootstrap buildx` pair from `publish-manifest`. `BUILDX_BUILDER` is read by the `docker buildx` CLI as the default-builder selection, so on `publish-manifest` (which intentionally creates no builder, since `imagetools create` / `inspect` are registry-side operations) every `docker buildx` invocation looked up a builder by that name and exited.

Move `BUILDX_BUILDER` out of workflow-level `env:` and into the `build` job's job-level `env:` block where the matching `setup-buildx-action` actually creates the builder. `publish-manifest` no longer sees the env var, falls back to the default builder, and `imagetools` runs as designed.

The break did not surface in PR-time CI because `publish-manifest` is gated to `push to main || (workflow_dispatch && main)` — neither runs on a `pull_request` event, so the publish path was effectively untested before merge. Two follow-up PRs harden against the same class of bug: a documentation PR adding a workflow-env-scoping rule and a smoke-test-from-feature-branch rule to PULL_REQUESTS.md, and a terraform PR raising the required-status-check list for jackin to include the new aggregator jobs.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request May 9, 2026
… rules (#267)

Codify two CI-merge-safety rules in `PULL_REQUESTS.md` plus a one-line cross-reference in `AGENTS.md`. The recent #256#266 incident exposed a class of break invisible to PR-time CI: a workflow-level env var (`BUILDX_BUILDER`) leaked into the `publish-manifest` job, where docker buildx read it as the default-builder selection and failed at runtime — but only on push to main, because that job is push-only and never runs on a `pull_request` event.

Three rules now live in `PULL_REQUESTS.md`. First, third-party-CLI env vars (`BUILDX_BUILDER`, `DOCKER_BUILDKIT`, `GH_TOKEN`/`GITHUB_TOKEN`, `KUBECONFIG`, `AWS_PROFILE`, `RUSTUP_TOOLCHAIN`, `npm_config_*`) must be scoped to the consuming job; workflow-level `env:` is reserved for in-house naming/paths. Second, registry / production publishing must hard-gate on main — the `is_publish` flag pattern from `construct.yml` is the canonical shape, and PRs / feature-branch dispatches must build-and-verify only, never publish. Third, push-only / main-only / `workflow_run`-gated jobs (`build-validator`, `publish-manifest`, `deploy`, `publish-preview`) must be smoke-tested via `gh workflow run --ref <feature-branch>` before requesting merge, since PR-time CI structurally cannot exercise them.

A companion terraform PR (jackin-project/jackin-github-terraform#14) raises the matching protection-rule changes — required-status-check list, single approving review, strict up-to-date branch policy.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit to jackin-project/jackin-github-terraform that referenced this pull request May 9, 2026
…iewer; strict up-to-date branch policy (#14)

Raise the merge gate for the `jackin` repo to match the path-aware-workflow refactor in jackin-project/jackin#256. Three changes.

`variables.tf` replaces the single `docs-link-check` entry with the four aggregator-style checks (`ci-required`, `construct-required`, `docs-required`, plus the still-direct `docs-link-check`), the new Renovate validate job (`validate`), and the existing `DCO` gate. The aggregators were added in #256 and roll up the path-aware-gated jobs in each workflow, so adding or removing a gated job inside any of those workflows in the future does not require a terraform change.

`branch-protection.tf` flips two policy knobs on the `protect_main` ruleset: `required_approving_review_count` from 0 to 1 (every PR now needs a human eyeball before merge) and `strict_required_status_checks_policy` from false to true (the PR branch must be up-to-date with main before merge so the green CI run reflects the exact code that lands).

This is the second half of the post-#266 hardening; the first half (the documentation rules in jackin-project/jackin#267) addresses what an agent reviewer should check at PR time, and this terraform change addresses what GitHub itself will let merge.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request May 18, 2026
Make the heavy CI workflows path-aware. Each of CI, Construct Image, Docs, and Publish Homebrew Preview keeps its push / pull_request / workflow_dispatch triggers so the GitHub check list stays stable for branch protection, but gates expensive jobs on a small `changes` classifier job. CI / Construct / Docs delegate path classification to `dorny/paths-filter@v4.0.1` (SHA-pinned). Publish Homebrew Preview keeps a small bespoke bash classifier because its diff base is the previously-published preview SHA read out of the live `jackin-preview.rb` formula on github.com — no off-the-shelf action wraps that, and reading the prior published SHA lets the classifier diff the full range that has accumulated since the last preview release rather than only the head commit's first parent. Manual `workflow_dispatch` short-circuits every classifier so operator-driven runs always exercise the heavy job regardless of which paths changed.

The Rust path filter covers `src/**`, `tests/**`, `Cargo.toml`, `Cargo.lock`, `rust-toolchain.toml`, `build.rs`, and `docker/runtime/**` — the latter two because `build.rs` supplies compile-time version metadata and `docker/runtime/entrypoint.sh` is `include_str!`'d by `src/derived_image.rs`, so both materially affect the produced binary. Docs gating splits into a cheap `repo-link-check` that runs on every non-schedule event (preserves the `<RepoFile />` rename / delete contract documented in `docs/AGENTS.md`) and an expensive `docs-link-check` gated on docs paths. Construct gating collapses the previously-duplicated `build-amd64` and `build-arm64` jobs into a single matrix `build` job and drops the unused `setup-buildx-action` + `Bootstrap buildx` pair from `publish-manifest` (it only runs `imagetools create`/`inspect`, both registry-side ops). The five-times-repeated `push || (workflow_dispatch && main)` boolean across construct.yml is hoisted into a single `is_publish` output on the `changes` job. The deployed-docs lychee link-check shared by docs.yml's `deploy` and `check-deployed` jobs is now `.github/actions/check-deployed-docs/action.yml`, with input-shape validation so a future caller cannot pass shell-special URLs into lychee args. Each path-aware workflow ends in a `*-required` aggregator job that calls the new `.github/actions/aggregate-needs/action.yml` composite action, giving branch protection a single stable check name regardless of which underlying jobs skipped.

`MISE_VERSION` is bumped from `2026.5.1` to `2026.5.3` (the older pin is no longer in the mise apt repo) and Renovate `customManagers` are added for `docker/construct/versions.env` so the `tirith` (github-releases), `shellfirm` (crates.io), and `mise` (deb datasource pointed at the apt repo the Dockerfile installs from) pins are auto-updated. A new `Renovate Validate` workflow asserts the deb registry URL still resolves and still lists the `mise` package on every PR, since Renovate's `deb` and `github-releases` datasources fail silently on a renamed upstream and would otherwise re-introduce the same drift.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request May 18, 2026
Bump `TIRITH_VERSION` from 0.2.12 to 0.3.1 in `docker/construct/versions.env`. Upstream 0.3.1 fixes an AWS access-key false-positive in S3 pre-signed URLs and SigV4 Authorization headers — the credential rule no longer flags `AKIA…` matches that sit inside actual SigV4 fields with non-empty signatures, while bare access keys and non-SigV4 contexts continue to fire.

This is the first Renovate-driven update produced by the customManagers added in #256, so it also doubles as a smoke test of that automation.

Signed-off-by: Renovate Bot <renovate@whitesourcesoftware.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request May 18, 2026
…de (#266)

Hotfix the construct-image break introduced by #256. After that PR merged, the `Construct Image / publish manifest` job on `main` failed with `ERROR: no builder "jackin-construct" found` because the refactor hoisted `BUILDX_BUILDER: jackin-construct` into the workflow-level `env:` block when it dropped the redundant `setup-buildx-action` / `Bootstrap buildx` pair from `publish-manifest`. `BUILDX_BUILDER` is read by the `docker buildx` CLI as the default-builder selection, so on `publish-manifest` (which intentionally creates no builder, since `imagetools create` / `inspect` are registry-side operations) every `docker buildx` invocation looked up a builder by that name and exited.

Move `BUILDX_BUILDER` out of workflow-level `env:` and into the `build` job's job-level `env:` block where the matching `setup-buildx-action` actually creates the builder. `publish-manifest` no longer sees the env var, falls back to the default builder, and `imagetools` runs as designed.

The break did not surface in PR-time CI because `publish-manifest` is gated to `push to main || (workflow_dispatch && main)` — neither runs on a `pull_request` event, so the publish path was effectively untested before merge. Two follow-up PRs harden against the same class of bug: a documentation PR adding a workflow-env-scoping rule and a smoke-test-from-feature-branch rule to PULL_REQUESTS.md, and a terraform PR raising the required-status-check list for jackin to include the new aggregator jobs.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request May 18, 2026
… rules (#267)

Codify two CI-merge-safety rules in `PULL_REQUESTS.md` plus a one-line cross-reference in `AGENTS.md`. The recent #256#266 incident exposed a class of break invisible to PR-time CI: a workflow-level env var (`BUILDX_BUILDER`) leaked into the `publish-manifest` job, where docker buildx read it as the default-builder selection and failed at runtime — but only on push to main, because that job is push-only and never runs on a `pull_request` event.

Three rules now live in `PULL_REQUESTS.md`. First, third-party-CLI env vars (`BUILDX_BUILDER`, `DOCKER_BUILDKIT`, `GH_TOKEN`/`GITHUB_TOKEN`, `KUBECONFIG`, `AWS_PROFILE`, `RUSTUP_TOOLCHAIN`, `npm_config_*`) must be scoped to the consuming job; workflow-level `env:` is reserved for in-house naming/paths. Second, registry / production publishing must hard-gate on main — the `is_publish` flag pattern from `construct.yml` is the canonical shape, and PRs / feature-branch dispatches must build-and-verify only, never publish. Third, push-only / main-only / `workflow_run`-gated jobs (`build-validator`, `publish-manifest`, `deploy`, `publish-preview`) must be smoke-tested via `gh workflow run --ref <feature-branch>` before requesting merge, since PR-time CI structurally cannot exercise them.

A companion terraform PR (jackin-project/jackin-github-terraform#14) raises the matching protection-rule changes — required-status-check list, single approving review, strict up-to-date branch policy.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant