report_project_versions
Report which task template version each configured project is actually on.
Projects are deployed independently, so no single workflow run or git tag describes what every project holds. This script reads each project's state from the hub and compares it against the config file the deployment workflow reads.
Task templates are the only part of this with real versioning: the hub assigns a
monotonic integer per owner/slug, and a project's latest task definition names the
one it is rendered from. Template variables have no version of their own, so they are
compared by a digest of their resolved values.
Example usage: python -m bitfount.runners.report_project_versions report \ task_templates/project-config-production-public.yaml --username myuser \ --password mypass --json-output report-bitfount.json
Render one markdown table from the per-account reports:
python -m bitfount.runners.report_project_versions summarise \ report-bitfount.json report-bitfount-testing.json
Module
Functions
fail_on_drift
def fail_on_drift(*json_files: str) ‑> None:Exit non-zero if any reported project disagrees with the config.
Shares DRIFT_STATUSES with the report itself, so the workflow gate and the
table cannot disagree about what counts.
Arguments
json_files: The JSON reports written byreport.
latest_template_version
def latest_template_version( task_owner: str, task_slug: str, hub: BitfountHub, hub_url: str, cache: dict[str, int | None],) ‑> int | None:The newest version of a task template, or None if it cannot be read.
Asking per project would repeat the same lookup for every project sharing a
template, so results are cached by owner/slug for the run.
Arguments
task_owner: The template owner.task_slug: The template slug.hub: The hub connection object.hub_url: The hub URL.cache: The per-run cache, keyed byowner/slug.
Returns The newest version, or None if the template could not be read.
report
def report( config_file: str, username: str, password: str | None = None, project_id: str | None = None, task_slug: str | None = None, trial_name: str | None = None, json_output: str | None = None, git_ref: str | None = None, fail_on_drift: bool = False,) ‑> None:Report what each configured project is actually on.
Arguments
config_file: Path to the project configuration file.username: The username to authenticate as. Only projects this user owns are reported; the others belong to another account's run.password: Optional password. Falls back to the device code flow.project_id: Only report this project id.task_slug: Only report projects using this task slug.trial_name: Only report projects whosetrial_namevariable is this.json_output: Optional path to write the report to as JSON, for a later step to render or post.git_ref: The git ref the config was read from, recorded in the report as the provenance of the desired state.fail_on_drift: Exit non-zero if any reported project has drifted.
summarise
def summarise(*json_files: str, markdown_output: str | None = None) ‑> None:Render one markdown table from the per-account JSON reports.
Arguments
json_files: The JSON reports written byreport.markdown_output: Optional path to write the markdown to. Written to stdout either way, so a workflow can append it to the job summary.
variables_digest
def variables_digest(variables: dict[str, Any]) ‑> str:A short, stable digest of a set of template variables.
Template variables carry no version, so a digest of their resolved values is the only way to say whether two sets are the same one. Keys are sorted so that a reordering of the config does not read as a change.
Arguments
variables: The variable names and values.
Returns
The first few hex characters of the digest, or empty for no variables.
Classes
ProjectVersionRow
class ProjectVersionRow( project_id: str, project_owner: str, desired_task_slug: str, desired_template_version: str, desired_variables_digest: str, actual_task_slug: str | None = None, actual_template_version: int | None = None, latest_template_version: int | None = None, actual_variables_digest: str | None = None, status: str = 'unreadable', detail: str = '',):One project's desired and actual state, as reported.
Variables
- static
actual_task_slug : str | None
- static
actual_template_version : int | None
- static
actual_variables_digest : str | None
- static
desired_task_slug : str
- static
desired_template_version : str
- static
desired_variables_digest : str
- static
detail : str
- static
latest_template_version : int | None
- static
project_id : str
- static
project_owner : str
- static
status : str
actual_label : str- Theslug@versionlabel for what the project is actually on.
ReportStatus
class ReportStatus(*args, **kwds):What the hub says about a project relative to the config.
Variables
- static
BEHIND_LATEST
- static
MATCHES
- static
NO_TASK_DEFINITION
- static
PINNED_BEHIND
- static
TEMPLATE_DIFFERS
- static
UNREADABLE
- static
VARIABLES_DIFFER
VersionReport
class VersionReport( account: str, config_file: str, git_ref: str | None = None, rows: list[ProjectVersionRow] = [],):Every row gathered for one account.
Variables
- static
account : str
- static
config_file : str
- static
git_ref : str | None
- static
rows : list[ProjectVersionRow]
Methods
drifted
def drifted(self) ‑> list[ProjectVersionRow]:The rows a reader needs to act on.
A pinned-behind row is where the config puts it, so it is reported in the
table but is not counted as drift.