repomatic.github.actions module¶
GitHub Actions output formatting, annotations, and workflow events.
This module provides utilities for working with GitHub Actions: multiline output formatting, workflow annotations, event payload loading, and GitHub-specific constants and enums shared across multiple modules.
Note
Concurrency quirks addressed by the workflows
SHA-based groups (``release.yaml``): the block sits on the
push-triggered entry workflow, not the reusable _release-engine.yaml it
calls. GitHub decides run cancellation from the entry workflow’s group, and
a block on the engine lane (reached via needs: build) joins its group only
after the build lane finishes, too late to cancel queued or building runs.
cancel-in-progress is evaluated on the new workflow, not the old one. If
a regular commit is pushed while a release workflow is running, the new
workflow would cancel it (same group). Solution: release commits (freeze and
unfreeze) get a unique group keyed by github.sha, so they can never be
cancelled.
Event-scoped groups (``changelog.yaml``): changelog.yaml has
both push and workflow_run triggers. Without event_name in
the concurrency group, a fast-completing workflow_run event would
cancel the push event’s prepare-release job, then skip
prepare-release itself (guarded by if: event_name != 'workflow_run'),
so it would never run. Including event_name prevents cross-event
cancellation.
``workflow_run`` checkout ref: Always use github.sha (latest
commit on the default branch), never workflow_run.head_sha (the
commit that triggered the upstream workflow). After a release cycle
adds commits (freeze + unfreeze), head_sha is stale and produces
a tree that conflicts with current main.
- repomatic.github.actions.NULL_SHA = '0000000000000000000000000000000000000000'¶
The null SHA used by Git to represent a non-existent commit.
GitHub sends this value as the
beforeSHA when a tag is created, since there is no previous commit to compare against.
- repomatic.github.actions.MAX_STEP_OUTPUT_BYTES = 130048¶
Ceiling for a single
$GITHUB_OUTPUTvalue, in UTF-8 bytes.A step output only exists to be read by a later step, and the two ways of reading one both land it in the consumer’s environment:
env:mapping asteps.*.outputs.*expression, and action inputs, which the runner exports asINPUT_*. Linux caps a singleargv/envpstring atMAX_ARG_STRLEN, 32 pages, so a value past that makes the runner’sexecve()of/usr/bin/bashfail withE2BIGbefore the step’s own command exists:##[error]An error occurred trying to start process '/usr/bin/bash' with working directory '/home/runner/work/orchard/orchard'. Argument list too long
The 1 KiB reserve covers the
NAME=prefix the kernel counts as part of the same string, well beyond the longest name in use.This ceiling only binds a value the environment has to carry. A report that grows without bound belongs in a file instead: see
format_file_output(), which hands the consumer a path and leaves the content untrimmed.Caution
This is a transport limit, counted in bytes, and is deliberately looser than
repomatic.github.pr_body.GITHUB_BODY_MAX_CHARS, which is a content limit counted in UTF-16 code units. A value can clear this one and still be trimmed later byrepomatic.github.pr_body.build_pr_body(), which is the layer that leaves the reader a truncation notice.
- class repomatic.github.actions.WorkflowEvent(*values)[source]¶
Bases:
StrEnumWorkflow events that cause a workflow to run.
- branch_protection_rule = 'branch_protection_rule'¶
- check_run = 'check_run'¶
- check_suite = 'check_suite'¶
- create = 'create'¶
- delete = 'delete'¶
- deployment = 'deployment'¶
- deployment_status = 'deployment_status'¶
- discussion = 'discussion'¶
- discussion_comment = 'discussion_comment'¶
- fork = 'fork'¶
- gollum = 'gollum'¶
- issue_comment = 'issue_comment'¶
- issues = 'issues'¶
- label = 'label'¶
- merge_group = 'merge_group'¶
- milestone = 'milestone'¶
- page_build = 'page_build'¶
- project = 'project'¶
- project_card = 'project_card'¶
- project_column = 'project_column'¶
- public = 'public'¶
- pull_request = 'pull_request'¶
- pull_request_comment = 'pull_request_comment'¶
- pull_request_review = 'pull_request_review'¶
- pull_request_review_comment = 'pull_request_review_comment'¶
- pull_request_target = 'pull_request_target'¶
- push = 'push'¶
- registry_package = 'registry_package'¶
- release = 'release'¶
- repository_dispatch = 'repository_dispatch'¶
- schedule = 'schedule'¶
- status = 'status'¶
- watch = 'watch'¶
- workflow_call = 'workflow_call'¶
- workflow_dispatch = 'workflow_dispatch'¶
- workflow_run = 'workflow_run'¶
- class repomatic.github.actions.AnnotationLevel(*values)[source]¶
Bases:
EnumAnnotation levels for GitHub Actions workflow commands.
Mirrors the three levels GitHub supports, even where the codebase only emits a subset.
- ERROR = 'error'¶
- WARNING = 'warning'¶
- NOTICE = 'notice'¶
- class repomatic.github.actions.ReportAction(*values)[source]¶
Bases:
EnumWhat a job did to one item, as the markdown report spells it.
Each member’s value is the emoji-decorated label the report table shows, so rendering reads the label straight off the action instead of consulting a parallel mapping a new member could silently miss.
The members are the union of the vocabularies every report needs, and each report uses the subset that applies to it: the changelog-to-release-notes sync (
release_sync) never unsubscribes, and the notification sweep (unsubscribe) has nothing to call in sync. What they share is the outcome pair every dry-runnable sweep reports, which is why the vocabulary is defined once here rather than re-spelled per report.- DRY_RUN = '👁️ Dry-run'¶
- FAILED = '⚠️ Failed'¶
- SKIPPED = '✅ In sync'¶
- UNSUBSCRIBED = '🔕 Unsubscribed'¶
- UPDATED = '🔄 Updated'¶
- repomatic.github.actions.extract_workflow_filename(workflow_ref)[source]¶
Extract the workflow filename from
GITHUB_WORKFLOW_REF.
- repomatic.github.actions.generate_delimiter()[source]¶
Generate a unique delimiter for GitHub Actions multiline output.
GitHub Actions requires a unique delimiter to encode multiline values in
$GITHUB_OUTPUT. This function generates a random delimiter that is extremely unlikely to appear in the output content.The delimiter format is
GHA_DELIMITER_NNNNNNNNNwhere N is a digit, producing a 9-digit random suffix.- Return type:
- Returns:
A unique delimiter string.
- repomatic.github.actions.trim_to_budget(text, budget, measure)[source]¶
Keep the leading whole lines of text that fit in budget.
Cutting on line boundaries keeps the trimmed markdown rendering: a table missing rows still renders, one cut mid-row does not.
The one trimming loop behind both of GitHub’s size ceilings, which count in different units: measure prices a line in whatever unit the caller’s budget is denominated in (UTF-8 bytes for a step output, UTF-16 code units for a PR or issue body).
- repomatic.github.actions.trim_to_byte_budget(text, budget)[source]¶
Keep the leading whole lines of text that fit in budget UTF-8 bytes.
trim_to_budget()in the step-output unit. Trimming whole lines also keeps the result valid UTF-8, which slicing a byte string cannot promise.
- repomatic.github.actions.format_multiline_output(name, value)[source]¶
Format a multiline value for GitHub Actions output.
Produces output in the heredoc format required by
$GITHUB_OUTPUT:name<<GHA_DELIMITER_NNNNNNNNN value line 1 value line 2 GHA_DELIMITER_NNNNNNNNN
Values over
MAX_STEP_OUTPUT_BYTESare trimmed to fit, so a step reading this output cannot be killed byE2BIGbefore it starts.
- repomatic.github.actions.write_output_file(name, value)[source]¶
Write a step output’s value to a file, and return that file’s path.
The file lands in
RUNNER_TEMPwhere the runner exports it, which is per-job and cleaned up by the runner itself, and in the system temporary directory otherwise. Either is shared by every step of a job, which is what makes the handoff work: producer and consumer are separate processes on one filesystem.
- repomatic.github.actions.format_file_output(name, value)[source]¶
Format a value as a file-backed output, for a consumer to read back.
Writes value out with
write_output_file()and names the step output<name>_file, so the consuming step receives a path instead of content:harvest_file=/home/runner/work/_temp/repomatic-harvest-8m1t0p.md
This is the escape hatch from
MAX_STEP_OUTPUT_BYTES. A report grows with the repository it describes and has no ceiling of its own: the release notes a dependency sweep collects reached 269 KiB on one downstream repo, twice what the environment can carry, and trimming to fit is a loss of content, not a fix. Handing over a path keeps the environment holding a hundred-odd bytes whatever the report weighs, and leaves any trimming torepomatic.github.pr_body.build_pr_body(), which is the layer that knows GitHub’s own body limit and marks the cut for the reader.
- repomatic.github.actions.emit_report(body, output, output_format, key='diff_table')[source]¶
Write a markdown report to
--output, optionally as a step output.The shared tail of every report-producing command: nothing is written when no output path is set or the body is empty; with
--output-format github-actionsthe body is spilled to a file and the step output named<key>_filecarries its path, for$GITHUB_OUTPUTconsumption.A report is the one value here with no ceiling of its own, so it is the one that must not travel inline: see
format_file_output().
- repomatic.github.actions.read_file_output(name)[source]¶
Read a value passed either as a file path or inline.
The consuming half of
format_file_output():<NAME>_FILEholds the path of a file whose content is the value, while<NAME>holds the value itself. The path wins where both are set, the inline variable remaining for a caller that has not moved over, and for a workflow pinned to a release older than the CLI it invokes.
- repomatic.github.actions.emit_annotation(level, message)[source]¶
Emit a GitHub Actions workflow annotation.
Prints a workflow command that creates an annotation visible in the GitHub Actions UI and PR checks.
- Parameters:
level (
AnnotationLevel) – The annotation level.message (
str) – The annotation message.
- Return type:
- repomatic.github.actions.get_github_event() dict[str, Any][source]¶
Load the GitHub event payload from
GITHUB_EVENT_PATH.
- repomatic.github.actions.get_event_pull_request()[source]¶
Return the event payload’s
pull_requestnode, empty when absent.Truthiness, not key presence, is the test every reader below shares. A payload carrying
pull_requestas an empty object has no PR to act on, and it has to read that way tois_pull_request()as well as to the default lookups: testing"pull_request" in eventhere (as this code once did) let the two disagree, sois_pull_requestreported a pull request whileget_default_number()fell through to the issue branch.
- repomatic.github.actions.get_event_subject()[source]¶
Return the issue or pull request the current event is about.
Pull requests win: the two nodes are mutually exclusive on the events these readers handle, and preferring the PR keeps the lookups reading the same node
is_pull_request()reports on.
- repomatic.github.actions.get_default_author()[source]¶
Get the issue/PR author from the GitHub event payload.
- repomatic.github.actions.get_default_number()[source]¶
Get the issue/PR number from the GitHub event payload.
- repomatic.github.actions.is_pull_request()[source]¶
Check if the current event is a pull request.
- Return type:
- repomatic.github.actions.cancel_superseded_runs(branch, current_run_id)[source]¶
Cancel the in-progress and queued workflow runs of branch.
Backs the
cancel-runscommand, fired when a pull request closes: GitHub’sconcurrencymechanism only cancels a run when a new run enters the same group, and closing a PR fires no such run, so the branch’s live runs would otherwise burn CI minutes to completion.Every listed run is cancelled except two: current_run_id (the cancelling run itself), and any run whose head commit carries
RELEASE_COMMIT_PREFIX. A run that fails to cancel (already finished, insufficient token scope) is logged and skipped so one straggler never aborts the sweep. The repository is resolved by theghCLI fromGH_REPOor the checkout, matching every othergh apicall.Caution
The release guard is what makes this safe to point at a default branch. Every workflow’s
cancel-in-progressgate already spares a release run from automatic supersession, but a sweep like this one enters no concurrency group, so nothing else would stop it from killing the matrix that publishes a release. Cancelling a release run mid-flight costs the version its binaries permanently, since publishing locks the asset list (claude.md§ A published release freezes what is missing from it).