tests.test_workflows module¶
Tests for GitHub Actions workflow files consistency.
- tests.test_workflows.METADATA_INDEX_RE = re.compile('fromJSON\\(needs\\.[\\w-]+\\.outputs\\.metadata\\)\\.(?P<key>\\w+)\\[')¶
A workflow expression that indexes into one key of the metadata output.
- tests.test_workflows.test_workflow_has_concurrency(workflow_name)[source]¶
Verify that required workflows have a concurrency block.
- Return type:
- tests.test_workflows.test_concurrency_group_format(workflow_name)[source]¶
Verify that concurrency group uses PR number or branch ref.
- Return type:
- tests.test_workflows.test_cancel_in_progress_protects_releases(workflow_name)[source]¶
Verify that cancel-in-progress protects release commits.
- Return type:
- tests.test_workflows.test_event_scoped_group_isolates_events(workflow_name)[source]¶
Verify that event-scoped workflows include event_name in the group.
- Return type:
- tests.test_workflows.test_unique_group_protects_releases(workflow_name)[source]¶
Verify workflows using unique groups protect releases via github.sha.
- Return type:
- tests.test_workflows.test_exempt_workflows_no_concurrency(workflow_name)[source]¶
Verify that exempt workflows really carry no concurrency block.
The exemption list must track reality: a workflow that gains a conformant block belongs in the derived
WORKFLOWS_WITH_CONCURRENCYset, where the group-format tests actually inspect it.- Return type:
- tests.test_workflows.TOOL_CACHE_KEY_SUFFIX = "${{ github.job_workflow_sha || hashFiles('repomatic/tooling/tool_registry.py') }}"¶
The only valid ending for a tool-cache key derived from the registry file.
- tests.test_workflows.test_tool_cache_keys_rotate_downstream()[source]¶
Every registry-derived cache key must work from a caller’s workspace.
The cache steps live in reusable workflows called cross-repo, where the checkout holds the caller’s tree:
hashFiles('repomatic/…')matches nothing there and returns an empty string, collapsing the key to a constant.actions/cacheskips the save on an exact primary-key hit, so a constant key freezes every downstream tool cache at its first write.github.job_workflow_shais the reusable workflow file’s own commit SHA, which rotates exactly when theuses:pin (and with it the pinned tool versions) moves, and is empty on direct upstream runs, where the file hash takes over.- Return type:
- tests.test_workflows.test_all_workflows_discovered()[source]¶
Verify that workflow discovery is working correctly.
- Return type:
- tests.test_workflows.test_exemption_list_matches_what_is_on_disk()[source]¶
Every exempt workflow really lacks a concurrency block, and vice versa.
The exemption set states an intention;
WORKFLOWS_WITHOUT_CONCURRENCY_BLOCKreads the files. Without this tie, a workflow could sit in the exempt list while carrying a block, which is howdebug.yamlwent unchecked: the parametrized tests skip it, and the test that should have noticed asserted only that its YAML parsed.- Return type:
- tests.test_workflows.test_every_job_caps_its_runtime()[source]¶
Every job that occupies a runner must declare
timeout-minutes.Without one, a hung step holds its runner for
GITHUB_DEFAULT_JOB_TIMEOUTminutes before the platform reclaims it. The macOS and Windows pools are capped per account and shared across every repository in it, so one stuck cell there starves all the others for the rest of those six hours: the cost of the omission lands on projects that have nothing to do with the workflow that hung.Callers of a reusable workflow are the one exemption, because GitHub rejects the key on them (see
REUSABLE_CALLER_KEYS). Their runtime is still bounded, by the caps on the jobs of the workflow they call, which this same sweep covers.- Return type:
- tests.test_workflows.test_job_timeouts_stay_within_bounds()[source]¶
No cap may be absent-by-another-name: zero, negative, or effectively the platform default.
A cap above
MAX_JOB_TIMEOUTbuys back almost none of the runner time the requirement exists to reclaim, so it needs the ceiling raised deliberately rather than one job quietly opting out.- Return type:
- tests.test_workflows.test_reusable_callers_carry_no_forbidden_keys()[source]¶
A
uses:job may only declareREUSABLE_CALLER_KEYS.GitHub fails the whole workflow at startup on any other key, so this catches a caller that grew a
timeout-minutes(or aruns-on, orsteps) before actionlint runs in CI.- Return type:
- tests.test_workflows.SITE_DEPLOY_COMPARISON_RE = re.compile("site_deploy\\s*==\\s*'([^']*)'")¶
A deploy-target literal a workflow gate compares
site_deployagainst.
- tests.test_workflows.test_deploy_gates_name_a_declared_target()[source]¶
Each
site.deploytarget has exactly the jobs it needs, and no more.SITE_DEPLOY_TARGETSpromises one deploy job per target, so the two halves must agree in both directions and neither is checkable from the other side. A gate naming a target the frozenset does not carry can never fire, and a workflow whose only deploy job is gated that way publishes nothing while running green:Config.__post_init__rejects the unknown value, but nothing reads the gate. A target declared with no gate behind it is the same failure approached from the other end, and is what a repository would hit the day it opted into it.- Return type:
- tests.test_workflows.test_version_bump_branches_ignored(workflow_name)[source]¶
Heavy PR-triggered workflows must list every VERSION_BUMP_BRANCHES member under
pull_request.branches-ignore.- Return type:
- tests.test_workflows.test_metadata_gate_skips_version_bump_branches(workflow_name)[source]¶
The
metadatajob’sif:expression must encode the same set of branch names as VERSION_BUMP_BRANCHES via acontains(fromJSON(...))check ongithub.head_ref.All PR-triggered workflows that ignore version-bump PRs need this gate because
pull_request.branches-ignoreonly filters the PR’s base branch (alwaysmain), so the head-ref check is the only mechanism that actually skips these PRs.- Return type:
- tests.test_workflows.test_tests_metadata_gate_skips_manual_bumps()[source]¶
tests.yaml’smetadatajob must short-circuit on every MANUAL_VERSION_BUMP_COMMIT_PREFIXES member, but must not gate on the[changelog] Post-release bumpprefix: that push carries the release commit, and the post-release-bump commit is the only post-merge test point for the release-frozen tree.- Return type:
- tests.test_workflows.test_metadata_gate_skips_version_bump_commits(workflow_name)[source]¶
The
metadatajob’sif:expression must short-circuit on the single CHANGELOG_COMMIT_PREFIX clause, so post-mergepushevents with any machine-authored version-machinery commit at their head skip the entire job graph. The prefix stands in for every VERSION_BUMP_COMMIT_PREFIXES member, whichtest_changelog_prefix_is_the_machinery_invariantholds.- Return type:
- tests.test_workflows.test_autofix_independent_jobs_repeat_version_bump_gate(job_name)[source]¶
Metadata-independent autofix jobs carry their own version-bump gate.
- Return type:
- tests.test_workflows.test_changelog_prefix_is_the_machinery_invariant()[source]¶
Every machine-authored version-machinery commit shape carries the prefix.
The single-clause workflow gates above are only sound while every version-bump commit message starts with CHANGELOG_COMMIT_PREFIX; this is the invariant that lets them stop enumerating message shapes.
- Return type:
- tests.test_workflows.test_version_bump_branches_match_changelog_workflow()[source]¶
VERSION_BUMP_BRANCHES must equal the set of branches actually created by
changelog.yaml’speter-evans/create-pull-requeststeps.prepare-releaseis hard-coded; thebump-versionjob templates${{ matrix.part }}-version-incrementovermatrix.part = [major, minor], so the full set is {prepare-release, major-version-increment, minor-version-increment}.- Return type:
- tests.test_workflows.test_bump_matrix_parts_are_the_ones_the_cli_accepts()[source]¶
changelog.yaml’s bump matrix may only name a part the CLI takes.The matrix value reaches
pr-sync --partandclose-stale-bump-pr --part, both typed onBUMP_PARTS. Adding a third part to the YAML alone would leave the job failing on a rejected value, in a workflow that only runs on a schedule.- Return type:
- tests.test_workflows.test_release_commit_prefix_in_changelog_workflow()[source]¶
Verify that changelog.yaml uses the same release commit prefix.
- Return type:
- tests.test_workflows.test_version_increments_runs_on_expected_events()[source]¶
Verify that bump-version job runs on all expected event types.
The bump-version job depends on the metadata job, which gates execution. It runs on push events (to recreate PRs before they conflict), schedule, workflow_dispatch, and successful workflow_run events.
- Return type:
- tests.test_workflows.test_prepare_release_runs_on_workflow_run()[source]¶
The prepare-release job must run on workflow_run events (the PR-refresh backstop).
workflow_runfires after every “🚀 Build & release” — so after every push tomain— and re-bases the release PR onto the current HEAD. Without it, a push that misses changelog.yaml’spaths:filter (a commit touching only skills, a composite action, or a markdown file the filter does not list) leaves the PR stale on its prior base. The job may excludeschedule(that trigger serves bump-version only) but must not excludeworkflow_run.- Return type:
- tests.test_workflows.test_workflow_run_references_match_workflow_names()[source]¶
Every
workflow_runtrigger must watch an existing workflow name.GitHub matches the
workflows:filter against the exactname:of the watched workflow, and silently drops the trigger when nothing matches. A rename is enough to kill it: adding emojis to workflow names in 2026-02 leftchangelog.yamlwatching the oldBuild & releasename, so its post-release re-trigger never fired again.- Return type:
- tests.test_workflows.test_bump_allowance_jobs_fetch_tags(job_name)[source]¶
Tag-sensitive changelog jobs must check out with
fetch-tags: true.is_version_bump_allowedresolves the latest release from Git tags. A job that evaluates it without fetching tags falls back to “allow”, which neutralizes theclose-stale-bump-prcleanup and leaves orphan version-bump PRs open onmain.- Return type:
- tests.test_workflows.test_post_release_commit_in_changelog_workflow()[source]¶
Verify that changelog.yaml uses the correct post-release commit message.
- Return type:
- tests.test_workflows.test_version_bump_commit_in_changelog_workflow()[source]¶
Verify that changelog.yaml uses the correct version bump commit message.
- Return type:
- tests.test_workflows.PR_SYNC_ENV_KEYS = frozenset({'GH_TOKEN'})¶
Values every
pr-syncstep passes throughenv:rather than itsrun:line.The token is here because
ghreads it from the environment and secrets are not ambient. Everything else the step used to relay (title, body, commit message, assignee) is now resolved insidepr-syncitself: rendered from the template, or read from ambient variables likeGITHUB_ACTOR. A step may declare more (bump-versionaddsPR_BRANCH/PR_PARTto keepmatrix.partout of its command line, the dependency steps a diff-table file), never fewer.
- tests.test_workflows.pr_sync_branch(step)[source]¶
Return the head branch a
pr-syncstep targets.Mirrors the CLI’s own resolution: an explicit
--branchwins (resolving the shell indirectionbump-versionuses to keepmatrix.partout of its command line), and otherwise the branch defaults to the template name.- Return type:
- tests.test_workflows.test_pull_requests_are_opened_by_the_cli_not_an_action()[source]¶
No workflow may open a pull request with a third-party action.
The drift guard for the migration off
peter-evans/create-pull-request: a new job copy-pasting the action from an old example fails here by name.- Return type:
- tests.test_workflows.test_pr_sync_steps_pass_everything_through_env()[source]¶
Every
pr-syncstep names a branch and takes its values fromenv:.- Return type:
- tests.test_workflows.test_pr_sync_templates_declare_their_labels()[source]¶
Every template a
pr-syncstep renders carries frontmatter labels.Labels moved from workflow YAML into template frontmatter; a template without them opens an unlabelled pull request, silently, so the gap is caught here by template name instead.
- Return type:
- tests.test_workflows.test_sync_repomatic_routes_upgrades_to_one_branch()[source]¶
sync-repomaticrunsinit --upgrade, and both outcomes share a branch.The
upgrade_versionoutput of theinitstep picks between the twopr-syncsteps, so exactly one runs, and both converge the same branch: a plain sync and an upgrade never stand as two pull requests.- Return type:
- tests.test_workflows.test_pr_sync_steps_are_template_driven()[source]¶
Every
pr-syncstep names a template rather than relaying rendered text.The template is what lets the command resolve title, body, commit message, branch, labels and draft state on its own; a step passing
--title/--bodyexplicitly would resurrect the$GITHUB_OUTPUTrelay and its step-output size ceiling.- Return type:
- tests.test_workflows.test_version_bumps_relock_before_committing()[source]¶
Every version bump must re-lock before the commit that captures it.
bump-my-versionrewritespyproject.tomlbut notuv.lock, so a bump committed without a followinguv lockleaves the tree with the project version ahead of its own lock entry.uv run --frozenthen reinstalls the project on the next sync to close that gap, and the release build runs two nested syncs: the workflow’s, then the onerepomatic runopens for aneeds_venvtool. The second deletes the console script the first had just written, which Windows refuses as a sharing violation and which took the Windows binaries out of a release.- Return type:
- tests.test_workflows.test_broken_links_skips_post_release_commits()[source]¶
Verify that broken-links job skips post-release version bump commits.
- Return type:
- tests.test_workflows.test_docs_trigger_covers_included_root_files()[source]¶
The docs
paths:filter must fire on each file the docs pull in.A page that includes a file from outside
docs/publishes content thedocs/**entry does not watch. With no entry of its own, a push touching just that file skips the workflow, and the page stays stale until something else touches a listed path. Downstream thin callers inherit this list verbatim, and their docs include the same root files.- Return type:
- tests.test_workflows.test_docs_trigger_covers_translated_readmes()[source]¶
The docs
paths:filter must fire on a translated readme.check-broken-linkscrawls every filemetadatareports underdoc_files, which globs all Markdown, so areadme.{lang}.mdis already inside the crawl. The trigger is what decides whether the crawl runs at all: withreadme.mdas the only readme entry, a push touching just a translation skipped the workflow, and those links went unchecked until something else happened to touch a listed path. Downstream thin callers inherit this list verbatim, so the gap reached every awesome list keeping a translation beside its English readme.- Return type:
- tests.test_workflows.test_cloudflare_deploy_fails_after_publishing_a_dropped_file()[source]¶
A dropped file is a dead link, so the job must end red, after the deploy.
The offload exits 1 when it drops a file, and
continue-on-errorlets the deploy publish everything else first. Only the last step, keyed on the offload’s outcome, turns the run red: without it, a dead link would sit behind a green run with a warning, which is how one stayed for years.- Return type:
- tests.test_workflows.iter_workflow_actions(workflow)[source]¶
Yield all action references (uses: statements) from a workflow.
- tests.test_workflows.iter_all_actions()[source]¶
Yield all action references from all workflow files.
Sorted so parametrize IDs are stable across processes (xdist collects in every worker and aborts on a mismatch).
- tests.test_workflows.test_action_uses_full_semantic_version(workflow_name, job_name, step_name, action)[source]¶
Verify that all actions use full semantic versions (vX.Y.Z), not major-only.
- Return type:
- tests.test_workflows.iter_jobs_with_runners()[source]¶
Yield all jobs with their runner configurations.
Sorted so parametrize IDs are stable across processes (xdist collects in every worker and aborts on a mismatch).
- tests.test_workflows.test_every_job_runs_on_a_test_axis(workflow_name, job_name, runs_on)[source]¶
Every literal
runs-on:names an image the test matrix also covers.There is no second set to be an exception to any more:
ubuntu-slimwas the last image outside the axes, and it lost the A/B that justified it. Deriving the expectation fromKNOWN_RUNNERSrather than from a literal keeps this from failing on a fleet migration instead of on the drift it exists to catch.- Return type:
- tests.test_workflows.test_binary_build_targets_are_the_test_axes()[source]¶
The Nuitka build fleet is exactly the set of images the suite runs on.
The companion to
test_every_job_runs_on_a_test_axis(), which can only see a literalruns-on:. Thecompile-binariesjob takes its runner from a matrix expression fed byNUITKA_BUILD_TARGETS, so that test skips it and the build fleet would otherwise be the one place an untracked image could sit unnoticed.The equality is the invariant, in both directions: a published binary is built on an image the suite is validated against, and an image the suite covers is one binaries can be built on. Adding a target therefore means widening the test axes in
repomatic.matrix_axes, never editing this fleet alone.binary.py’s own docstring states the rule; this holds it.- Return type:
- tests.test_workflows.test_prerelease_python_label_reaches_the_job_name()[source]¶
The test job titles itself from
python-label, falling back to the version.repomatic show-metadataattachespython-labelto the cells whose Python is unreleased and to no others (seePRERELEASE_LABEL_SUFFIX), so the fallback is not decoration: on every released cell the key resolves to an empty string, and the job would be titledpywith nothing after it. The metadata half of the contract is held bytests/test_metadata.py::test_unstable_python_versions_carry_a_prerelease_label.- Return type:
- tests.test_workflows.test_resolution_cell_is_read_by_the_test_job()[source]¶
A matrix cell carrying a
resolutionkey resolves the project with it.[tool.repomatic] test-matrix.full-includeadds such a cell to run every declared dependency floor. With no step reading the key, the cell installs the lockfile like its twin and verifies nothing. With no trace of the key in the job name, the two cells cannot be told apart.- Return type:
- tests.test_workflows.test_lowest_versions_cell_runs_on_the_floor_python()[source]¶
This repository runs one stable
lowest-directcell, on its lowest Python.The oldest dependencies meet the oldest interpreter there, which is the environment a declared floor promises to support. When the Python floor moves, this cell has to move with it.
- Return type:
- tests.test_workflows.test_workflows_index_structured_metadata_only()[source]¶
A workflow indexes a metadata key only when its value is a JSON structure.
show-metadata --format github-jsonjoins a list of strings with spaces (seeformat_github_value()). An expression that indexes such a string gives an empty value and raises nothing, so a step gated oncli_scripts[0]is skipped on every run without a word.- Return type:
- tests.test_workflows.iter_jobs_with_steps()[source]¶
Yield
(workflow_name, job_name, steps)for every job that has steps.Reusable-workflow calls (
uses:jobs) carry nosteps, so they are skipped: they inherit no toolchain and provision none.
- tests.test_workflows.test_uv_invoking_jobs_discovered()[source]¶
The uv-invocation matcher must find jobs (guards against a dead regex).
- Return type:
- tests.test_workflows.test_uv_invoking_job_provisions_setup_uv(workflow_name, job_name)[source]¶
Every job that runs
uv/uvxmust provision uv viaastral-sh/setup-uv.GitHub Actions jobs share no state: each runs on a fresh runner and inherits nothing from the jobs before it, so a job that shells out to uv must install uv itself. This cycle’s
cancel-runs.yamlrewrite from a bash body to arepomaticCLI call dropped the job’ssetup-uvstep, and every PR-close run failed withuvx: command not found. This test locks the invariant across all workflows so a uv-invoking job can never ship without its own uv provisioning again.- Return type:
- tests.test_workflows.iter_all_conditions()[source]¶
Yield
(file_path, line, condition)for everyif:in the Actions tree.Covers the composite actions alongside the workflows: both ship downstream and GitHub Actions evaluates their
if:by the same rule.Sorted so parametrize IDs are stable across processes (xdist collects in every worker and aborts on a mismatch).
- tests.test_workflows.test_conditions_discovered()[source]¶
The condition walker must find conditions (guards against a dead walk).
- Return type:
- tests.test_workflows.test_condition_wrapper_spans_the_whole_value(file_path, line, condition)[source]¶
A condition using
${{ }}holds nothing outside its braces.GitHub Actions evaluates an
if:value as an expression only when the value is entirely one${{ … }}. Any character outside the braces switches it to string interpolation, and the resulting non-empty string is truthy, so the step silently stops gating and runs unconditionally.A folded block scalar is the trap, because it appends a trailing newline: a wrapped expression under
if: >becomes that expression plus a newline, and the character outside the braces is invisible in the diff. Eleven steps of thesync-depsjob inautofix.yamlwere gated this way, all of them always-on until this test was written.Write a multi-line condition bare instead, with no wrapper at all: a bare
if:value is evaluated as an expression whatever whitespace trails it. A leading!(as in!cancelled()) rules out the bare single-line form, since YAML reads it as a tag indicator, so those stay either folded-and-bare or single-line-and-wrapped.- Return type:
- tests.test_workflows.test_all_workflows_have_symlinks_in_data()[source]¶
Verify that every exportable workflow has a symlink in repomatic/data/.
- Return type:
- tests.test_workflows.test_only_workflows_agents_and_actions_are_symlinks_in_data()[source]¶
Verify only workflow, agent, and composite-action files are symlinks in repomatic/data/.
Scoped to the top level of
repomatic/data/. Skills live one directory down, as a folder per skill, and are covered bytest_skill_symlinks_resolve_correctly().- Return type:
- tests.test_workflows.test_workflow_symlinks_resolve_correctly()[source]¶
Verify that workflow symlinks in repomatic/data/ point to the correct targets.
- Return type:
- tests.test_workflows.test_action_symlinks_resolve_correctly()[source]¶
Verify that composite-action symlinks in repomatic/data/ resolve correctly.
Uses the registry as the source of truth: every
FileEntrywhose target sits under.github/actions/must be backed by a symlink at the declaredsourcepath that resolves to the declared target file.- Return type:
- tests.test_workflows.test_skill_symlinks_resolve_correctly()[source]¶
Verify each bundled skill folder mirrors its
.claude/skills/original.A skill ships as a whole folder, so
repomatic/data/skills/{id}/must be a real directory whose every leaf is a symlink to the file of the same relative path under.claude/skills/.uv_buildrefuses a symlinked directory in package data and fails the wheel, while symlinked files are dereferenced into it normally.- Return type:
- tests.test_workflows.test_agent_symlinks_resolve_correctly()[source]¶
Verify that agent symlinks in repomatic/data/ point to the correct targets.
- Return type:
- tests.test_workflows.test_release_build_matrix_outputs_degrade_to_empty_string()[source]¶
_release-build.yamlmatrix outputs must emit''(not"null") for null.GitHub Actions serializes a null value through
toJSON(...)as the literal string"null", which is truthy in expressions. A caller that guards itsstrategy.matrixwith... || '{"include":[]}'then never reaches the fallback, andfromJSON("null")aborts the run withUnexpected value ''. Every*_matrixworkflow_call output must therefore degrade to an empty string for a null source (the<value> && toJSON(<value>) || ''pattern), so the caller’s fallback fires and the job skips cleanly.- Return type:
- tests.test_workflows.test_release_lanes_split_jobs()[source]¶
The build lane owns the package build; the engine lane owns the rest.
Splitting
build-package(and the squash guard) into_release-build.yamlis what lets release.yaml’s publish-pypi depend on the wheel alone. The engine keeps the binary, tag, and release-finalization jobs and must no longer definebuild-package.- Return type:
- tests.test_workflows.test_shippability_gate_blocks_the_build_lane()[source]¶
build-packagemust stay downstream of thelint-depsgate.The gate is only a gate through this edge. A
lint-depsjob that merely reports leavesbuild-packagerunning, which setspackage_builtand lets the caller’spublish-pypiship a wheel nobody can install: a[tool.uv.sources]override never reaches the published metadata, so there is no other job in the lane that could notice.Skipping
build-packageis what closes the whole lane.package_builtresolves false, sopublish-pypinever fires, and the failed lane skips the engine call, takingcreate-tag,create-releaseandpublish-releasewith it.- Return type:
- tests.test_workflows.test_publish_pypi_does_not_touch_the_github_release()[source]¶
The publish-pypi lane must not edit the GitHub release (ordering race).
The GitHub release is created by the engine’s
create-releasejob, several jobs deeper than the caller’s fastpublish-pypijob. Agh release editin publish-pypi therefore raced the release’s creation, failed with “release not found”, and (undercontinue-on-error) silently dropped the PyPI availability admonition. The admonition is now baked into the notes at draft creation, so publish-pypi owns only the PyPI upload and needs nocontentswrite.- Return type:
- tests.test_workflows.test_create_release_bakes_pypi_admonition_at_creation()[source]¶
The engine bakes the PyPI availability admonition into the draft notes.
create-releasewrites the notes while the release is still a draft, so the admonition lands there: this avoids both the cross-lane race (above) and the immutable-release locks that apply oncepublish-releaseflips the draft. The body falls back to the plain notes for non-PyPI projects, whoserelease_notes_with_admonitionis empty.- Return type:
- tests.test_workflows.test_release_engine_lane_is_reusable(lane)[source]¶
Each RELEASE_ENGINE_WORKFLOWS lane is a reusable workflow shipped here.
The generated
release.yamlcalls these lanes by cross-repouses:, so each must exist in.github/workflows/and declare aworkflow_calltrigger.- Return type:
- tests.test_workflows.test_release_engine_lanes_not_materialized_downstream()[source]¶
The engine lanes are referenced cross-repo, never deployed as files.
They must not be downstream
FileEntryfile_ids (the release entry’s file_id isrelease.yaml), and the entry’s recorded source must be one of the lanes soWORKFLOW_SOURCESand the concept stay consistent.- Return type:
- tests.test_workflows.COOLDOWN_EXEMPT_JOBS: dict[str, frozenset[str]] = {'tests.yaml': frozenset({'test-package-install'})}¶
Jobs allowed to override the workflow-wide cooldown, and why.
test-package-installinstalls the freshly published artifact on purpose, so a cooldown makes the question it exists to answer unanswerable. Seedocs/workflows.md§ Install-time cooldown for the full exemption roster.
- tests.test_workflows.test_workflow_declares_cooldown_env(workflow)[source]¶
Every workflow carries the cooldown
env:block, verbatim.The block is rendered from
[tool.repomatic] minimum-release-age, so this is what keeps the YAML literal tied to its single source of truth: YAML cannot read the config itself. A workflow without it silently resolves packages published seconds ago.- Return type:
- tests.test_workflows.test_cooldown_block_reads_true_downstream()[source]¶
The cooldown comment cites the published page, never a repository file.
The block lands above the
jobs:line of every generated workflow, in repositories that hold none of this project’s files.- Return type:
- tests.test_workflows.test_arch_aliases_mirror_extra_platforms()[source]¶
tests.yaml’s runner check carries extra-platforms’ aliases verbatim.The check runs on the runner image’s own interpreter, with no dependency install: reaching
extra_platformsmeans resolving a Python through uv, which transparently prefers an x86_64 interpreter on aarch64, the very thing that job exists to catch. So the alias data is copied into the YAML, and this pins the copy to its source the waytest_workflow_declares_cooldown_env()pins the cooldown block.- Return type:
- tests.test_workflows.CLI_INVOCATION_RE = re.compile('uvx\\s+--no-progress\\s+--from\\s+\\.\\s+repomatic|uv\\s+--no-progress\\s+run\\s+--frozen\\s+--\\s+repomatic')¶
Any way a workflow can invoke this project’s CLI from the local checkout.
- tests.test_workflows.test_local_cli_invocation_is_contiguous(workflow)[source]¶
Every local CLI invocation is the exact one-line form the freeze rewrites.
freeze_cli_versionis a literal string replacement, so an invocation a YAML folded scalar wraps across two lines is invisible to it: the release would ship that step still pointing at the local checkout, which downstream has no copy of. The same blind spot hides the step from a line-oriented grep, which is how one survived the migration offuvx --from .and failed only once CI reached it.Keep the invocation on one line and wrap elsewhere in the command.
- Return type:
- tests.test_workflows.test_local_cli_jobs_discovered()[source]¶
The local-CLI matcher must find jobs (guards against a dead constant).
- Return type:
- tests.test_workflows.test_local_cli_job_checks_out_repo(workflow_name, job_name)[source]¶
Every job running the CLI from
uv.lockmust check out the repository.uv run --frozenresolves the project from the working directory, so without a checkout there is nopyproject.toml, nouv.lock, and no console script: the step dies onFailed to spawn: repomatic.The
publish-releasejob shipped without one on the assumption that the freeze had rewritten its invocation to the PyPI-resolveduvxform. It had, on the release commit — but a rebase-merged release PR pushes the freeze and unfreeze commits together, and Actions runs the workflow at the push head, so the unfrozen form is what executes upstream. Seerepomatic/release/prepare_release.pyfor the contract.- Return type:
- tests.test_workflows.test_checkout_precedes_artifact_download(workflow_name, job_name)[source]¶
A job downloading run artifacts must check out before it downloads.
actions/checkoutdeletes the contents of its target directory whenever that directory holds no.gitof its own, which is exactly the state a fresh workspace is in afteractions/download-artifacthas written to it:Deleting the contents of ‘/home/runner/work/{repo}/{repo}’
So the download silently loses everything it fetched, and the job carries on against an empty tree.
publish-releaseacquired a checkout below its binary download this way, which would have left the next release an unpublished draft with no binary attached.Reversing the two is the whole fix: a checkout into a pristine workspace has nothing to delete, and the download then writes into the checked-out tree.
- Return type:
- tests.test_workflows.test_cooldown_override_is_declared(workflow)[source]¶
No job weakens the cooldown without being listed as a known exemption.
A job-level
env:silently outranks the workflow-level block, so an undeclared override is the one way the cooldown can disappear while every other check still passes.- Return type:
- tests.test_workflows.test_every_setup_uv_step_pins_a_version(path)[source]¶
Every
setup-uvstep declares the uv version it installs.Two reasons this must hold everywhere rather than mostly. Without the input,
setup-uvinstalls the newest uv satisfyingrequired-version, so the tool enforcing every cooldown arrives without one. Andrepomatic.release.version_sync._SETUP_UV_VERSION_REscans lazily from theuses:line to the nextversion:, so a step missing the input would silently borrow the following step’s andsync-workflow-pinswould rewrite the wrong line.- Return type:
- tests.test_workflows.test_setup_uv_pins_agree()[source]¶
All
setup-uvpins name one uv version.sync-workflow-pinsresolves a single version per package, so a split pin would leave one of the two silently un-bumped.- Return type:
- tests.test_workflows.test_report_step_outputs_are_consumed()[source]¶
A report emitted to a step output must be read back somewhere.
--output-format github-actionsis the hand-off shape: the command spills a report to a file and names it as a<key>_filestep output, for a later step to feed into a pull request body (REPOMATIC_DIFF_TABLE_FILE,--template-arg-file).format-imageslost its summary table to exactly that gap in7.11.0: the step emittingmarkdown_filesurvived the collapse of the PR-publishing steps, but its consumer did not, and a body-less template rendered as if the report never existed. An output nobody references is that orphan shape by construction, whatever the key.- Return type:
- tests.test_workflows.RELEASE_ASSET_ARTIFACT_PREFIX = 'release-asset-'¶
Prefix the engine’s
extra-assetsjob matches run artifacts on.
- tests.test_workflows.test_plugin_archive_is_a_declared_release_asset()[source]¶
The packed plugin’s filename is one of the declared release assets.
pack-pluginwritesARCHIVE_NAMEby default andrelease.yamlpasses no--output, so renaming the constant on its own would leaverelease-assetsand the uploadpath:naming a file no job produces. The test below then holds that filename equal to the run-artifact name and theneeds:edge, which is what makes the handover checkable end to end from a Python constant TOML and YAML cannot read.- Return type:
- tests.test_workflows.test_declared_release_assets_are_space_free()[source]¶
No declared filename can contain a space.
The engine passes the list through a space-separated environment variable and word-splits it in bash, so a space silently turns one filename into two and the completeness check passes vacuously.
- Return type:
- tests.test_workflows.test_declared_release_asset_has_a_producing_job(asset)[source]¶
Every declared asset is produced and handed over by a job in release.yaml.
Three spellings have to agree for the handover to work, in three files no single tool reads at once: the filename in
[tool.repomatic] release-assets, therelease-asset-<filename>run-artifact name, and theneeds:edge that makes the artifact exist before the engine looks for it. Any one of them drifting fails the release atextra-assets, or worse, publishes an immutable release without the asset.- Return type: