-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add combine-durations composite action (#126)
Originally implemented in conda/conda we port the combine-durations composite action over to conda/actions so other repositories (like conda/conda-build) can also benefit from this workflow. This workflow will load the duration artifacts from the 10 most recent runs for a given workflow, aggregate and average the durations, commit the updated duration files, and finally open a PR with the changes.
- Loading branch information
1 parent
5cb7ed2
commit c1c5971
Showing
5 changed files
with
187 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Combine Durations | ||
|
||
A composite GitHub Action to combine the duration files from recent pytest runs. | ||
|
||
## GitHub Action Usage | ||
|
||
In your GitHub repository include this action in your workflows: | ||
|
||
```yaml | ||
- uses: conda/actions/combine-durations | ||
with: | ||
# [optional] | ||
# the git branch to search | ||
branch: main | ||
|
||
# [optional] | ||
# the glob pattern to search for duration files | ||
pattern: '*-all' | ||
|
||
# [optional] | ||
# the GitHub token with artifact downloading permissions | ||
token: ${{ github.token }} | ||
|
||
# [optional] | ||
# the workflow to search | ||
workflow: tests.yml | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
"""Combine test durations from all recent runs.""" | ||
from __future__ import annotations | ||
|
||
import json | ||
from argparse import ArgumentParser, ArgumentTypeError, Namespace | ||
from functools import partial | ||
from pathlib import Path | ||
from statistics import fmean | ||
|
||
from rich.console import Console | ||
|
||
print = Console(color_system="standard", soft_wrap=True).print | ||
|
||
|
||
def validate_dir(value: str, writable: bool = False) -> Path: | ||
try: | ||
path = Path(value).expanduser().resolve() | ||
path.mkdir(parents=True, exist_ok=True) | ||
if writable: | ||
ignore = path / ".ignore" | ||
ignore.touch() | ||
ignore.unlink() | ||
return path | ||
except (FileExistsError, PermissionError) as err: | ||
# FileExistsError: value is a file, not a directory | ||
# PermissionError: value is not writable | ||
raise ArgumentTypeError(f"{value} is not a valid directory: {err}") | ||
|
||
|
||
def parse_args() -> Namespace: | ||
# parse CLI for inputs | ||
parser = ArgumentParser() | ||
parser.add_argument("--durations-dir", type=validate_dir, required=True) | ||
parser.add_argument( | ||
"--artifacts-dir", | ||
type=partial(validate_dir, writable=True), | ||
required=True, | ||
) | ||
return parser.parse_args() | ||
|
||
|
||
def main() -> None: | ||
args = parse_args() | ||
|
||
# aggregate new durations | ||
combined: dict[str, dict[str, list[float]]] = {} | ||
for path in args.artifacts_dir.glob("**/*.json"): | ||
os_combined = combined.setdefault((OS := path.stem), {}) | ||
|
||
new_data = json.loads(path.read_text()) | ||
for key, value in new_data.items(): | ||
os_combined.setdefault(key, []).append(value) | ||
|
||
# aggregate old durations | ||
for path in args.durations_dir.glob("*.json"): | ||
try: | ||
os_combined = combined[(OS := path.stem)] | ||
except KeyError: | ||
# KeyError: OS not present in new durations | ||
print(f"⚠️ {OS} not present in new durations, removing") | ||
path.unlink() | ||
continue | ||
|
||
old_data = json.loads(path.read_text()) | ||
if missing := set(old_data) - set(combined[OS]): | ||
for name in missing: | ||
print(f"⚠️ {OS}::{name} not present in new durations, removing") | ||
|
||
# only copy over keys that are still present in new durations | ||
for key in set(old_data) & set(combined[OS]): | ||
os_combined[key].append(old_data[key]) | ||
|
||
# drop durations no longer present in new durations and write out averages | ||
for OS, os_combined in combined.items(): | ||
(args.durations_dir / f"{OS}.json").write_text( | ||
json.dumps( | ||
{key: fmean(values) for key, values in os_combined.items()}, | ||
indent=4, | ||
sort_keys=True, | ||
) | ||
+ "\n" # include trailing newline | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
name: Combine Durations | ||
description: Combine the duration files from recent pytest runs. | ||
author: Anaconda Inc. | ||
branding: | ||
icon: book-open | ||
color: green | ||
|
||
inputs: | ||
branch: | ||
description: The branch to search for recent pytest runs. | ||
default: main | ||
durations-dir: | ||
description: The directory to write the combined durations file. | ||
default: durations | ||
pattern: | ||
description: The pattern to search for recent pytest runs. | ||
default: '*-all' | ||
token: | ||
description: >- | ||
A token with ability to comment, label, and modify the commit status | ||
(`pull_request: write` and `statuses: write` for fine-grained PAT; `repo` for classic PAT) | ||
default: ${{ github.token }} | ||
workflow: | ||
description: The workflow to search for recent pytest runs. | ||
default: tests.yml | ||
|
||
runs: | ||
using: composite | ||
steps: | ||
- name: download recent artifacts | ||
shell: bash | ||
run: > | ||
gh run list | ||
--branch ${{ inputs.branch }} | ||
--workflow ${{ inputs.workflow }} | ||
--limit 10 | ||
--json databaseId | ||
--jq '.[].databaseId' | ||
| xargs -n 1 | ||
gh run download | ||
--dir ${{ runner.temp }}/artifacts/ | ||
--pattern '${{ inputs.pattern }}' | ||
|| true | ||
env: | ||
GITHUB_TOKEN: ${{ github.token }} | ||
|
||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 | ||
with: | ||
path: ~/.cache/pip | ||
# invalidate the cache anytime a workflow changes | ||
key: ${{ hashFiles('.github/workflows/*') }} | ||
|
||
- uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 | ||
with: | ||
python-version: '3.11' | ||
|
||
- name: install dependencies | ||
shell: bash | ||
run: pip install --quiet -r ${{ github.action_path }}/requirements.txt | ||
|
||
- name: combine recent durations from artifacts | ||
shell: bash | ||
run: > | ||
python ${{ github.action_path }}/action.py | ||
--durations-dir=${{ inputs.durations-dir }} | ||
--artifacts-dir=${{ runner.temp }}/artifacts/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
rich |