Internal Documentation
Documentation for EpiAwarePackageTools's internal interface.
Contents
Index
EpiAwarePackageTools.ReusableRefSourceEpiAwarePackageTools._applyEpiAwarePackageTools._apply_agent_fileEpiAwarePackageTools._apply_git_blame_ignoreEpiAwarePackageTools._apply_gitignoreEpiAwarePackageTools._apply_org_brandingEpiAwarePackageTools._apply_pagesEpiAwarePackageTools._apply_standard_sectionsEpiAwarePackageTools._detect_ad_setup_ownedEpiAwarePackageTools._detect_benchmark_history_parkedEpiAwarePackageTools._detect_benchmarksEpiAwarePackageTools._detect_docs_subdomainEpiAwarePackageTools._detect_doiEpiAwarePackageTools._detect_downgrade_compatEpiAwarePackageTools._detect_licenseEpiAwarePackageTools._detect_managed_overrideEpiAwarePackageTools._detect_org_brandingEpiAwarePackageTools._detect_reviewerEpiAwarePackageTools._detect_unregistered_sourcesEpiAwarePackageTools._docstr_textEpiAwarePackageTools._kit_git_pin_gapEpiAwarePackageTools._logo_initialEpiAwarePackageTools._preserve_action_pinsEpiAwarePackageTools._preserve_downstreamsEpiAwarePackageTools._preserve_reusable_refsEpiAwarePackageTools._render_badgesEpiAwarePackageTools._resolve_docs_subdomainEpiAwarePackageTools._tutorials_subdirEpiAwarePackageTools._warn_local_caller_override!EpiAwarePackageTools._workspace_manifest_pathEpiAwarePackageTools.run_package_tests
Internal API
EpiAwarePackageTools.ReusableRefSource Type
struct ReusableRefSource{L, N}ReusableRefSource(latest, is_newer)Where update learns the current ref of a shared reusable workflow. Those workflows live in EpiAware/.github, so neither callback is told which repository to look in.
latest(workflow)returns the newest commit SHA touching.github/workflows/<workflow>there, ornothingwhen that cannot be resolved.is_newer(current, latest)returnstruewhenlatestis strictly ahead ofcurrentin that repository's history,falsewhen it is not (identical, behind, or diverged), andnothingwhen the question cannot be answered — e.g. either SHA is unknown there, as for one copied from a fork.
Injected rather than called directly so the freshening policy is testable without network. The default, _gh_ref_source(), shells out to gh.
Fields
latest::Anyis_newer::Any
EpiAwarePackageTools._apply Function
_apply(
target_dir::AbstractString;
managed_only,
force,
ad,
benchmarks,
downgrade_compat,
unregistered_sources,
inputs,
freshen_reusable_refs,
ref_source
)_apply(target_dir; managed_only, force, ad, benchmarks,
downgrade_compat, unregistered_sources, inputs,
freshen_reusable_refs, ref_source)Shared worker for scaffold/update.
managed_only restricts to managed templates (the update path). force overwrites package-owned files too (only meaningful for scaffold). ad selects the AD-enabled or AD-disabled standard; benchmarks gates the opt-in benchmark CI/suite/docs page; downgrade_compat gates the opt-in downgrade-compat CI job; unregistered_sources declares that the package pins an unregistered dependency by git [sources], which holds it to the Julia 1.11 floor. freshen_reusable_refs opts into moving each managed caller's reusable-workflow ref forwards (see _RefFreshener), with ref_source the injection point tests stub.
Returns a (created, updated, preserved, removed, warnings) manifest of destination paths. removed holds the retired managed paths cleaned up (see RETIRED_PATHS); warnings the non-fatal issues raised while applying, e.g. a diverged-but-unmarked test/ad/setup.jl about to be overwritten.
EpiAwarePackageTools._apply_agent_file Function
_apply_agent_file(
target_dir::AbstractString,
template,
inputs
) -> Tuple{Symbol, Bool}_apply_agent_file(target_dir, template, inputs)Apply a managed pointer block to target_dir/$template.
AGENTS.md points at the human-facing docs; CLAUDE.md points at AGENTS.md. Neither restates a standard, so there is one copy of each and it cannot drift.
Returns (action, changed) where action is :created, :injected (markers added to a file the package already had, whose content is kept below the block), or :refreshed (markers present; only the marked region is touched). Mirrors _apply_gitignore.
EpiAwarePackageTools._apply_git_blame_ignore Function
_apply_git_blame_ignore(
target_dir::AbstractString
) -> Tuple{Symbol, Bool}_apply_git_blame_ignore(target_dir)Apply the managed .git-blame-ignore-revs header block to target_dir.
Returns (action, changed) where action is :created, :injected (markers added to an existing file), or :refreshed (markers already present; only the marked region is touched). Mirrors _apply_gitignore.
EpiAwarePackageTools._apply_gitignore Function
_apply_gitignore(
target_dir::AbstractString,
inputs::NamedTuple
) -> Tuple{Symbol, Bool}_apply_gitignore(target_dir, inputs)Apply the managed .gitignore block to target_dir.
Returns (action, changed) where action is :created, :injected (markers added to an existing file, e.g. on first run of a kit version with this fix), or :refreshed (markers already present; only the marked region is touched). Mirrors _apply_badges.
EpiAwarePackageTools._apply_org_branding Function
_apply_org_branding(
target_dir::AbstractString,
org_branding::Bool
) -> Symbol_apply_org_branding(target_dir, org_branding)Write (or remove) the bundled EpiAware org logo asset, following the package's ORG_BRANDING opt-in (#242).
Not a SCAFFOLD_TEMPLATES entry, like the LICENSE variants: the table is emitted wholesale, and a third-party adopter must not be handed an EpiAware logo.
Returns :created, :refreshed (drifted, rewritten), :unchanged, :removed (branding off, the kit's asset withdrawn), or :skipped (branding off, nothing of ours there), so update is a fixed point in both states.
Turning branding off deletes the asset only when it is byte-identical to the one the kit shipped. A file the package put at that path is not the kit's to remove, so it is left alone with a warning.
sourceEpiAwarePackageTools._apply_pages Function
_apply_pages(
target_dir::AbstractString,
inputs::NamedTuple
) -> Tuple{Symbol, Union{Nothing, String}}_apply_pages(target_dir, inputs)Apply the managed docs/pages.jl to target_dir.
docs/pages.jl is MANAGED, but "managed" means something narrower here than for any other template: update/scaffold only regenerate it when the committed file already carries _MANAGED_PAGES_MARKER in its header, or no file exists yet. A committed file without the marker is preserved untouched no matter what force says – see the section header above for why. The regenerated content splices EXTENSIONS_NAV/ BENCHMARKS_NAV from inputs (computed in _apply, unchanged fragments) and the four docs_config.jl extension points read fresh from disk here, so a package's docs_config.jl edits and a force reset of it (which happens before this runs) are both reflected.
Returns (action, warning): action is :created (no prior file), :refreshed (marker present, content changed), :unchanged (marker present, content already current) or :preserved (no marker); warning is the migration message from _pages_groups_at_risk, or nothing.
EpiAwarePackageTools._apply_standard_sections Function
_apply_standard_sections(
target_dir::AbstractString,
inputs::NamedTuple;
org_branding
) -> Tuple{Symbol, Bool}_apply_standard_sections(target_dir, inputs)Inject or refresh the managed README standard-sections block.
Returns (action, changed) where action is :refreshed (markers present), :injected (appended to a README carrying none of these sections yet), or :skipped (no README, missing inputs, or a marker-less README that already has bespoke prose for one of them — migrating that is a deliberate per-repo wording change, #67). As in _apply_badges, only the marked region is rewritten.
EpiAwarePackageTools._detect_ad_setup_owned Function
_detect_ad_setup_owned(target_dir::AbstractString) -> Bool_detect_ad_setup_owned(target_dir)Whether a package has opted its AD-harness driver (test/ad/setup.jl) out of kit management by marking it package-owned (#162).
test/ad/setup.jl is force-managed: update() overwrites it with the generic driver, which assumes the package's ADFixtures registry satisfies the current ADRegistry contract (its scenarios accepts a category keyword). A package whose ADFixtures predates that contract would MethodError on category=, so it must keep its own driver while it migrates. A comment containing EPIAWARE_AD_SETUP_OWNED in the committed file tells update() to preserve it. An unmarked file is managed as before.
EpiAwarePackageTools._detect_benchmark_history_parked Function
_detect_benchmark_history_parked(
target_dir::AbstractString
) -> Bool_detect_benchmark_history_parked(target_dir)Whether a package has parked benchmark-history.yaml's push/tag triggers, so a resync (update) preserves that state instead of re-enabling a permanently failing history run (#153).
benchpkg installs the package into a temp environment where a [sources] pin does not apply, so an unregistered [sources]-pinned dependency never resolves there and every push/tag-triggered history run fails. Parking drops the push/tags triggers, keeping only workflow_dispatch, until the package is registered. The committed on: block is the marker: parked iff it carries no push:. A target with no file defaults to the full triggers.
EpiAwarePackageTools._detect_benchmarks Function
_detect_benchmarks(target_dir::AbstractString) -> Bool_detect_benchmarks(target_dir)Whether a repo already has benchmarks enabled, so a resync (update with no benchmarks kwarg) preserves an adopter's opt-in instead of stripping their benchmark CI/suite/page (the #72 trap).
The scheduled template-sync bakes benchmarks = into its update call, but a repo scaffolded before the flag re-passes nothing, so the state must also be recoverable from the destination. The managed benchmark CI workflows are the marker. A never-scaffolded target has neither and so defaults to opt-out.
EpiAwarePackageTools._detect_docs_subdomain Function
_detect_docs_subdomain(
target_dir::AbstractString
) -> Union{Nothing, String, Symbol}_detect_docs_subdomain(target_dir)Recover the docs-hosting choice from an already-scaffolded repo so a resync (update with no docs_subdomain kwarg) keeps it instead of silently reverting a subdomain-hosted package to project-pages (#123).
The managed docs/make.jl carries the resolved deploy_url literal: a quoted host means the custom-subdomain path, a bare nothing means project-pages. Returns the host string, nothing (explicit project-pages), or :missing when docs/make.jl is absent or carries no deploy_url, so the caller falls back to the scaffold default.
When deploy_url = nothing but the repo has a gh-pages CNAME (a Pages custom domain set out of band) the two disagree: the site is served from the CNAME root while the build uses the /<Repo>.jl/ base, so every asset 404s and the docs render unstyled. CI cannot see it — the deploy passes — so the CNAME is consulted in that one case and its host recovered as the subdomain.
EpiAwarePackageTools._detect_doi Function
_detect_doi(
target_dir::AbstractString
) -> Union{Tuple{Nothing, Nothing}, Tuple{String, String}}_detect_doi(target_dir)Recover a persisted Zenodo DOI and badge id from an already-scaffolded repo so a resync (update with no doi/zenodo_badge kwargs) keeps an adopter's DOI badge instead of stripping it (#161).
The README "License & DOI" badge cell is fully managed and re-rendered on every sync, but doi/zenodo_badge default to nothing and the scheduled template-sync never re-passes them, so the values must be read back from the destination — exactly as _detect_reviewer recovers the code-owner handle. Reads the managed DOI badge the kit renders ([](https://doi.org/<doi>)) back from the existing README and returns the (doi, zenodo_badge) pair, or (nothing, nothing) when the README is absent or carries no DOI badge (so a never-configured repo stays unconfigured).
EpiAwarePackageTools._detect_downgrade_compat Function
_detect_downgrade_compat(target_dir::AbstractString) -> Bool_detect_downgrade_compat(target_dir)Whether a repo keeps the opt-in downgrade-compat CI job, so a resync (update with no downgrade_compat kwarg) does not reintroduce a job the package deliberately removed (#121).
A package pinned to a Julia floor, or one adopting an unregistered [sources]-pinned dependency, can never resolve julia-downgrade-compat, so it disables the job in its test.yaml; regenerating it on every sync would reintroduce a permanently-red job. The committed downgrade.yml caller is the marker. A target with no test.yaml defaults to keeping the job.
EpiAwarePackageTools._detect_license Function
_detect_license(
target_dir::AbstractString
) -> Union{Nothing, String}_detect_license(target_dir)The SPDX licence identifier target_dir declares, in the canonical spelling of its SUPPORTED_LICENSES entry, or nothing when it declares none.
The scheduled sync runs update with no license keyword, so the value has to be read back from the destination or the managed README badge would be rewritten to "MIT" on every run. Matching is case-insensitive, so a differently-spelled declaration still resolves.
The declaration is the top-level Project.toml license field. A value outside SUPPORTED_LICENSES throws, since the badge is rendered from it and the default would publish a licence claim the package never made. Pass license explicitly to relabel a package on purpose.
With no field to read, the managed README badge supplies the value. The badge is kit output rather than a declaration, so a label naming no supported licence is warned about and ignored, leaving the sync free to rewrite it.
sourceEpiAwarePackageTools._detect_managed_override Function
_detect_managed_override(
target_dir::AbstractString,
dest::AbstractString,
rendered::AbstractString
) -> Bool_detect_managed_override(target_dir, dest, rendered)Whether the template-emitted managed file at dest has been marked package-owned, so update() preserves it rather than resyncing it (#224).
Managed files always resync, which is what keeps an adopter on the current standard. A package that must keep its own version of one says so in the file, by putting EPIAWARE_MANAGED_OVERRIDE in a comment. The match is a plain case-sensitive occursin, so a mis-cased marker does nothing.
This governs whole template-emitted files only. The marker-delimited regions the kit injects into otherwise package-owned files (the .gitignore block, the README badge and standard-sections blocks, [workspace]) are refreshed by their own appliers, which never consult this.
rendered is the freshly rendered template for dest, required rather than defaulted: a managed template that itself contained the marker literal would otherwise hand every adopter a self-preserving copy and the kit would stop managing its own file everywhere. So a render carrying the marker keeps the file managed, and the test suite asserts no bundled template renders it.
test/ad/setup.jl also still honours its original marker EPIAWARE_AD_SETUP_OWNED (#162); either opts that file out.
scaffold/scaffold_generate (force = true) ignore the marker, so a new package always starts managed. The marker opts a file out of resyncing, not of retirement: a RETIRED_PATHS entry is still deleted.
EpiAwarePackageTools._detect_org_branding Function
_detect_org_branding(target_dir::AbstractString) -> Bool_detect_org_branding(target_dir)Whether the package opted in to EpiAware org branding, via const ORG_BRANDING = true in the package-owned docs/docs_config.jl (#242).
Read from the destination rather than passed as a kwarg, the same detect-from-the-file idempotency as _detect_benchmarks, so an update (or the scheduled sync, which passes no kwargs) preserves the package's choice. Defaults to off for a package with no config, or one predating the key.
EpiAwarePackageTools._detect_reviewer Function
_detect_reviewer(
target_dir::AbstractString
) -> Union{Nothing, String}_detect_reviewer(target_dir)Recover a persisted reviewer handle from an already-scaffolded repo so a resync (update with no reviewer kwarg) keeps it instead of reverting to the org placeholder (#72).
CODEOWNERS is managed and the scheduled template-sync never re-passes reviewer, so the handle is read back from the destination, as _preserve_reusable_refs does for reusable-workflow refs. Returns the first @handle on the active CODEOWNERS owner line (leading @ stripped, an org/team slug kept whole), or nothing when CODEOWNERS is absent or carries only the commented placeholder.
EpiAwarePackageTools._detect_unregistered_sources Function
_detect_unregistered_sources(
target_dir::AbstractString
) -> Bool_detect_unregistered_sources(target_dir)Whether a repo pins an unregistered dependency by git [sources], so a resync (update with no unregistered_sources kwarg) preserves the Julia 1.11 floor that pin genuinely needs (#410).
[sources] is silently ignored before 1.11, so a package pinning an ecosystem sibling that is not yet registered really cannot support 1.10: an LTS job would resolve whatever a registry carries, or fail to resolve at all. That package keeps the floor. Every other package does not, because the standard itself no longer pins anything by git.
The kit's own name is skipped: a leftover git pin on EpiAwarePackageTools is the thing _kit_git_pin_gap asks the package to remove (#361), not a reason to hold it at the floor.
EpiAwarePackageTools._docstr_text Function
_docstr_text(docstr) -> Any_docstr_text(docstr)Render one DocStr's text vector to a string, keeping only the authored prose.
A DocStr.text is a vector of pieces: plain interpolation splits it into several AbstractString fragments, and a registered @template wraps each docstring as [Template{:before}, "<prose>", Template{:after}], so the prose is an interior element, not the last one. Joining only the AbstractString/Markdown.MD pieces and dropping the Template markers means a templated package reads the same as a plain one.
EpiAwarePackageTools._kit_git_pin_gap Function
_kit_git_pin_gap(
target_dir::AbstractString
) -> Union{Nothing, String}_kit_git_pin_gap(target_dir)The warning for a package still pinning EpiAwarePackageTools by git [sources], or nothing when none does (#361).
The kit is registered in General, so a managed environment depends on the released version through [compat]. A leftover git pin resolves an unreleased main instead, drags the environment onto the Julia 1.11 floor [sources] needs, and blocks the package's own registration — a registrable repo cannot reference an unreleased GitHub package from any TOML.
Run after the templates are applied, so the managed test/jet/Project.toml has already been fixed by the sync and is not reported. What remains is package-owned (test/Project.toml, test/ad, docs, benchmark) or carries an ownership marker, which update will not rewrite — hence a warning rather than a silent fix.
EpiAwarePackageTools._logo_initial Function
_logo_initial(pkg::Union{Nothing, AbstractString}) -> Any_logo_initial(pkg)The single glyph shown on the placeholder logo (templates/docs/src/assets/logo.svg): the package's first letter, uppercased, or "?" when the package name is unknown, which keeps scaffold_inputs total.
EpiAwarePackageTools._preserve_action_pins Function
_preserve_action_pins(
content::AbstractString,
dest::AbstractString
) -> Any_preserve_action_pins(content, dest)Keep the destination's existing third-party action pins when re-emitting a managed workflow.
Dependabot owns the github-actions pins in every adopting repo, so a template that hard-pins actions/checkout@v7 would revert a Dependabot bump on every resync — and when template-sync re-applies on a branch it did not open, that revert rides silently into the merge (#215). The destination's pin wins and only the rest of the workflow is re-applied. Mirrors _preserve_reusable_refs, which owns the org reusable-caller lines.
EpiAwarePackageTools._preserve_downstreams Function
_preserve_downstreams(
content::AbstractString,
dest::AbstractString
) -> Any_preserve_downstreams(content, dest)Keep the destination's reverse-dependency list when re-emitting the managed downstream.yaml.
Which packages depend on this one is a fact about the adopting package, not a standard the kit sets, but re-applying the template's downstreams: '[]' seed reset an adopter's list on every sync (#234). So the committed value wins, as in _preserve_reusable_refs. Preferable to marking the whole file package-owned, which would stop it tracking the standard for one line.
A bespoke pass rather than a _preserve_caller_with_inputs case: that keys off the uses:→with:→secrets: shape, which this caller does not have (it puts secrets: first), and the template renders the key itself so _merge_with_blocks would let the seed win (#183).
EpiAwarePackageTools._preserve_reusable_refs Function
_preserve_reusable_refs(
content::AbstractString,
dest::AbstractString;
freshener
) -> Any_preserve_reusable_refs(content, dest; freshener = nothing)Keep the destination's existing reusable-workflow refs when re-emitting a managed CI caller.
Dependabot owns the EpiAware/.github reusable SHAs in every adopting repo, so a hard-pinned template would report drift every time Dependabot moved the live pin. When the destination already pins a ref for the same reusable, that ref wins and only the rest of the caller body is re-applied; on first adoption the template's seed ref is used.
A freshener (see _RefFreshener) then offers each surviving ref the newest commit that touched the workflow it names, and takes it only when the ref held here is strictly older. Preserving is still the floor: freshening can move a ref forwards, never back.
EpiAwarePackageTools._render_badges Function
_render_badges(
repo::AbstractString,
pkg::AbstractString;
ad,
license,
docs_url,
doi,
zenodo_badge
)_render_badges(repo, pkg; ad, license = DEFAULT_LICENSE,
docs_url = nothing, doi = nothing, zenodo_badge = nothing)Render the standard badge block (without the markers) from resolved inputs.
repo is the owner/name.jl slug; pkg the package name; ad adds the per-backend AD CI + coverage badge table; license is the SPDX id whose badge is shown. doi/zenodo_badge add a Zenodo DOI badge when both are given. The layout is a five-column header table (Documentation, Build Status, Code Quality, License & DOI, Downloads) plus the per-backend AD table. Every URL is built from repo/pkg, so no owner/repo is hardcoded.
EpiAwarePackageTools._resolve_docs_subdomain Function
_resolve_docs_subdomain(_::Nothing, pkg)_resolve_docs_subdomain(spec, pkg)Resolve the docs_subdomain input to either nothing (project-pages, the default) or a concrete host string.
true selects the conventional <pkg>.epiaware.org; a string is taken verbatim; nothing/false opt out. The Bool and Nothing cases dispatch to their own methods so the String conversion only runs on a genuine string input, which keeps JET type-stable.
EpiAwarePackageTools._tutorials_subdir Function
_tutorials_subdir(target_dir::AbstractString) -> String_tutorials_subdir(target_dir)Read TUTORIALS_SUBDIR from the package-owned docs/docs_config.jl: the subdir (relative to docs/src) holding the Literate tutorial sources and their rendered .md pages.
The managed .gitignore ignores those rendered pages, so it must track whatever path the package configures. The const is written as a quoted string or a joinpath of quoted segments; every quoted segment is joined with /. Falls back to the template default when the config is absent or omits it.
EpiAwarePackageTools._warn_local_caller_override! Function
_warn_local_caller_override!(
warnings::Vector{String},
to::AbstractString,
dest::AbstractString
)_warn_local_caller_override!(warnings, to, dest)Warn when the committed to (the managed workflow at relative path dest) carries a caller job repointed at a repo-local reusable workflow with its own with: inputs — a shape _preserve_caller_with_inputs cannot see, so the resync silently drops those inputs and reverts the job to the shared reusable (#325).
_CALLER_JOB matches only uses: <org>/.github/.github/workflows/<file>@<ref>. A package needing an input the shared reusable does not expose points the caller at a local copy instead, which never matches, so _emit re-renders the job from the template and the inputs are gone with nothing to say so.
This does not change what is emitted; it only makes the loss visible in warnings (and via @warn), one message per matching job, so it surfaces in the sync PR rather than as an unrelated-looking red check days later.
EpiAwarePackageTools._workspace_manifest_path Function
_workspace_manifest_path(
target_dir::AbstractString,
subdir::AbstractString;
version
) -> String_workspace_manifest_path(target_dir, subdir; version = VERSION)The Manifest.toml path Pkg resolves for the <subdir>/Project.toml environment, whether or not that file exists. version is the Julia version the answer is wanted for.
From Julia 1.12 a subdirectory named in the root Project.toml's [workspace] projects shares the root resolve, so the root Manifest.toml is where Pkg writes and reads that environment's dependencies and <subdir>/Manifest.toml plays no part in it. Base's code loading falls back to <subdir>/Manifest.toml only when the root Manifest.toml is absent, so the shared root manifest is the oracle wherever one exists. This is the managed shape _apply_workspace writes, sharing one resolve across test and docs. Earlier Julia versions have no [workspace] support and always read <subdir>/Manifest.toml.
EpiAwarePackageTools.run_package_tests Function
run_package_tests(; ...) -> Any
run_package_tests(
testdir::AbstractString;
filter,
verbose
) -> Anyrun_package_tests(testdir = pwd(); filter = nothing, verbose = false)Run the @testitems of the package whose test/ tree is testdir.
Deprecated
Call TestItemRunner's @run_package_tests directly instead. Discovery is scoped by the managed JuliaTestItems.toml at the package root, so this function no longer does anything @run_package_tests does not:
using TestItemRunner
@run_package_tests filter = ti -> !(:ad in ti.tags)Kept so a package scaffolded before that config file keeps working after a sync, since test/runtests.jl is package-owned and never rewritten.