Skip to content

Commit 9e5d934

Browse files
committed
ci: add device list update workflow and script
1 parent 5480596 commit 9e5d934

3 files changed

Lines changed: 196 additions & 6 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""
2+
This module contains helper methods to retrieve a list of all devices in a directory.
3+
It does so by recursively traversing the directory and checking if the file contains a
4+
class that inherits from the Device class.
5+
"""
6+
7+
import importlib.util
8+
import os
9+
10+
from ophyd import Device
11+
12+
13+
def get_devices(repo_name: str, ignore: str = "") -> dict:
14+
"""
15+
Get all devices in a directory.
16+
17+
Args:
18+
directory (str): The directory to search for devices.
19+
ignore (str): A comma-separated list of module names to ignore.
20+
21+
Returns:
22+
list: A list of all devices in the directory.
23+
"""
24+
devices = {}
25+
26+
anchor_module = importlib.import_module(f"{repo_name}.devices")
27+
directory = os.path.dirname(anchor_module.__file__)
28+
29+
for root, _, files in os.walk(directory):
30+
for file in files:
31+
if not file.endswith(".py") or file.startswith("__"):
32+
continue
33+
34+
path = os.path.join(root, file)
35+
subs = os.path.dirname(os.path.relpath(path, directory)).split("/")
36+
if len(subs) == 1 and not subs[0]:
37+
module_name = file.split(".")[0]
38+
else:
39+
module_name = ".".join(subs + [file.split(".")[0]])
40+
41+
if module_name in ignore.split(","):
42+
continue
43+
module = importlib.import_module(f"{repo_name}.devices.{module_name}")
44+
45+
for name in dir(module):
46+
obj = getattr(module, name)
47+
if not hasattr(obj, "__module__") or obj.__module__ != module.__name__:
48+
continue
49+
if isinstance(obj, type) and issubclass(obj, Device) and obj is not Device:
50+
devices[obj.__name__] = obj
51+
52+
return dict(sorted(devices.items(), key=lambda x: x[0].lower()))
53+
54+
55+
def write_device_list(devices: dict, file_name: str, repo_name: str, append=False):
56+
"""
57+
Write the list of devices to a file.
58+
59+
Args:
60+
devices (list): A list of devices.
61+
file_out (str): The file to write the devices to.
62+
repo_name (str): The repository name for linking to the source code.
63+
append (bool): Whether to append to the file or overwrite it.
64+
"""
65+
if not append or not os.path.exists(file_name):
66+
with open(file_name, "w", encoding="utf-8") as output_file:
67+
output_file.write("// This file was autogenerated. Do not edit it manually.\n")
68+
output_file.write("## Device List\n")
69+
70+
with open(file_name, "a", encoding="utf-8") as output_file:
71+
output_file.write(f"### {repo_name} \n")
72+
output_file.write("| Device | Documentation | Module |\n")
73+
output_file.write("| :----- | :------------- | :------ |\n")
74+
for dev, cls in devices.items():
75+
doc = cls.__doc__
76+
doc = "" if doc is None else doc.replace("\n", "<br>")
77+
output_file.write(
78+
f"| {dev} | {doc} | [{cls.__module__}](https://github.com/bec-project/{repo_name}/tree/main/{cls.__module__.replace('.', '/')}.py) |\n"
79+
)
80+
81+
82+
if __name__ == "__main__":
83+
import argparse
84+
85+
parser = argparse.ArgumentParser(description="Retrieve a list of devices in a directory.")
86+
parser.add_argument("repo", type=str, help="The repository to link to.")
87+
parser.add_argument("output", type=str, help="The file to write the devices to.")
88+
parser.add_argument(
89+
"--append", action="store_true", help="Append to the file instead of overwriting it."
90+
)
91+
parser.add_argument(
92+
"--ignore", type=str, help="Ignore the specified modules (comma-separated).", default=""
93+
)
94+
95+
args = parser.parse_args()
96+
devs = get_devices(args.repo, ignore=args.ignore)
97+
write_device_list(devs, args.output, args.repo, append=args.append)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
name: Update device list
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
device_list_update:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
concurrency:
14+
group: device-list-update-${{ github.ref_name }}
15+
cancel-in-progress: true
16+
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0 # required for git diff / reset
22+
ssh-key: ${{ secrets.CI_DEPLOY_SSH_KEY }}
23+
ssh-known-hosts: ${{ secrets.CI_DEPLOY_SSH_KNOWN_HOSTS }}
24+
25+
- name: Set up Python
26+
uses: actions/setup-python@v5
27+
with:
28+
python-version: "3.11"
29+
30+
- name: Setup | Force release branch to be at workflow sha
31+
run: |
32+
git reset --hard ${{ github.sha }}
33+
34+
- name: Evaluate | Verify upstream has NOT changed
35+
shell: bash
36+
run: |
37+
set +o pipefail
38+
39+
UPSTREAM_BRANCH_NAME="$(git status -sb | head -n 1 | cut -d' ' -f2 | grep -E '\.{3}' | cut -d'.' -f4)"
40+
printf '%s\n' "Upstream branch name: $UPSTREAM_BRANCH_NAME"
41+
42+
set -o pipefail
43+
44+
if [ -z "$UPSTREAM_BRANCH_NAME" ]; then
45+
printf >&2 '%s\n' "::error::Unable to determine upstream branch name!"
46+
exit 1
47+
fi
48+
49+
git fetch "${UPSTREAM_BRANCH_NAME%%/*}"
50+
51+
if ! UPSTREAM_SHA="$(git rev-parse "$UPSTREAM_BRANCH_NAME")"; then
52+
printf >&2 '%s\n' "::error::Unable to determine upstream branch sha!"
53+
exit 1
54+
fi
55+
56+
HEAD_SHA="$(git rev-parse HEAD)"
57+
58+
if [ "$HEAD_SHA" != "$UPSTREAM_SHA" ]; then
59+
printf >&2 '%s\n' "[HEAD SHA] $HEAD_SHA != $UPSTREAM_SHA [UPSTREAM SHA]"
60+
printf >&2 '%s\n' "::error::Upstream has changed, aborting release..."
61+
exit 1
62+
fi
63+
64+
printf '%s\n' "Verified upstream branch has not changed, continuing with release..."
65+
66+
- name: Install Python dependencies
67+
run: |
68+
pip install .
69+
70+
- name: Generate device list
71+
run: |
72+
python .github/scripts/retrieve_device_classes.py \
73+
"ophyd_devices" \
74+
"./ophyd_devices/devices/device_list.md" \
75+
--ignore areadetector.plugins
76+
77+
- name: Commit and push if device list changed
78+
run: |
79+
FILE="./ophyd_devices/devices/device_list.md"
80+
81+
if [ -f "$FILE" ]; then
82+
git add "$FILE"
83+
84+
git config user.name "github-actions[bot]"
85+
git config user.email "github-actions[bot]@users.noreply.github.com"
86+
87+
if ! git diff-index --quiet HEAD --; then
88+
git commit -m "docs: Update device list"
89+
90+
git push origin "${{ github.ref_name }}"
91+
92+
echo "Device list updated"
93+
else
94+
echo "No changes detected"
95+
fi
96+
else
97+
echo "Device list file not found"
98+
fi

.github/workflows/semantic_release.yml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ on:
99
permissions:
1010
contents: read
1111

12-
13-
1412
jobs:
1513
release:
1614
runs-on: ubuntu-latest
@@ -39,7 +37,7 @@ jobs:
3937
- name: Set up Python
4038
uses: actions/setup-python@v5
4139
with:
42-
python-version: '3.11'
40+
python-version: "3.11"
4341

4442
- name: Setup | Force release branch to be at workflow sha
4543
run: |
@@ -49,9 +47,6 @@ jobs:
4947
# the upstream branch while this workflow was running. This is important
5048
# because we are committing a version change (--commit). You may omit this step
5149
# if you have 'commit: false' in your configuration.
52-
#
53-
# You may consider moving this to a repo script and call it from this step instead
54-
# of writing it in-line.
5550
shell: bash
5651
run: |
5752
set +o pipefail

0 commit comments

Comments
 (0)