Skip to content

Commit a468509

Browse files
fix(release_homebrew): reduce test duplication (#238)
1 parent e77d4c0 commit a468509

4 files changed

Lines changed: 323 additions & 257 deletions

File tree

actions/release_homebrew/README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# release_homebrew
22

3-
A reusable action to audit, install, test, and publish homebrew formulas to a tap.
3+
A reusable action to audit, bottle build, test, and publish homebrew formulas to a tap.
44

55
This action is designed to work with formulas supplied by the upstream repository, instead of the tap repository.
66
This works better for projects where the build process often changes, and formulas are more challenging to update.
77

8-
As part of an automated release workflow, this action can be used to audit, install, test, and publish the supplied
8+
As part of an automated release workflow, this action can be used to audit, bottle build, test, and publish the supplied
99
formulas to the tap repository.
1010

1111
Additionally, this action can be used to contribute the formula to an upstream homebrew-core repository by
@@ -60,8 +60,12 @@ steps:
6060

6161
| Name | Description |
6262
|-----------|---------------------------------------------------|
63-
| buildpath | The path to Homebrew's temporary build directory. |
64-
| testpath | The path to Homebrew's temporary test directory. |
63+
| testpath | The action-owned directory where test artifacts from the bottle build are copied. |
64+
65+
During validation, the action sets `HOMEBREW_TEMP` and `HOMEBREW_TEST_ARTIFACTS_DIR` for
66+
`brew test-bot --only-formulae`. Formulae that need artifacts such as gtest XML or coverage reports can write them to
67+
`HOMEBREW_TEST_ARTIFACTS_DIR`, which is under Homebrew's temp directory so macOS bottle builds can write to it from the
68+
sandbox. After `test-bot` finishes, the action copies those files to `testpath`.
6569

6670
## 🧰 Advanced Usage
6771

@@ -85,7 +89,7 @@ steps:
8589
publish: true # you probably want to use some conditional logic here
8690
token: ${{ secrets.PAT }} # required to publish
8791
upstream_homebrew_core_repo: Homebrew/homebrew-core
88-
validate: false # skip the audit and install steps
92+
validate: false # skip audit and bottle validation
8993
```
9094

9195
## 📝 Notes

actions/release_homebrew/action.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: "Release Homebrew"
3-
description: "A reusable action to audit, install, test, and publish a Homebrew formula."
3+
description: "A reusable action to audit, bottle build, test, and publish a Homebrew formula."
44
author: "LizardByte"
55

66
branding:
@@ -80,11 +80,8 @@ inputs:
8080
required: false
8181

8282
outputs:
83-
buildpath:
84-
description: "The path to Homebrew's temporary build directory."
85-
value: ${{ steps.homebrew-tests.outputs.buildpath }}
8683
testpath:
87-
description: "The path to Homebrew's temporary test directory."
84+
description: "The action-owned directory where test artifacts from the bottle build are copied."
8885
value: ${{ steps.homebrew-tests.outputs.testpath }}
8986
commit_message:
9087
description: "The commit message used for the formula update."

actions/release_homebrew/main.py

Lines changed: 149 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import stat
77
import subprocess
88
import sys
9+
import tempfile
910
from typing import AnyStr, IO, Optional, Mapping
1011

1112
# lib imports
@@ -21,20 +22,20 @@
2122
# result placeholder
2223
ERROR = False
2324
FAILURES = []
24-
TEMP_DIRECTORIES = []
25-
HOMEBREW_BUILDPATH = ""
25+
HOMEBREW_TEMP_ENV_VAR = "HOMEBREW_TEMP"
26+
TEST_ARTIFACTS_ENV_VAR = "HOMEBREW_TEST_ARTIFACTS_DIR"
2627

2728
tap_repo_name = "" # will be set based on INPUT_ORG_HOMEBREW_REPO
2829

2930
og_dir = os.getcwd()
3031

3132

3233
def _parse_args(args_list: list) -> argparse.Namespace:
33-
parser = argparse.ArgumentParser(description='Homebrew formula audit, install, and test')
34+
parser = argparse.ArgumentParser(description='Homebrew formula audit, bottle build, and test')
3435
parser.add_argument(
3536
'--formula_file',
3637
default=os.environ['INPUT_FORMULA_FILE'],
37-
help='Homebrew formula file to audit, install, and test',
38+
help='Homebrew formula file to audit, bottle build, and test',
3839
type=str,
3940
)
4041
return parser.parse_args(args_list)
@@ -892,7 +893,137 @@ def brew_test_bot_only_tap_syntax() -> bool:
892893
return result
893894

894895

895-
def brew_test_bot_only_formulae(formula: str) -> bool:
896+
def get_test_artifacts_dir() -> str:
897+
"""
898+
Get the action-owned directory where formulae can write test artifacts.
899+
900+
Returns
901+
-------
902+
str
903+
Absolute path to the test artifacts directory.
904+
"""
905+
return os.path.abspath(
906+
os.path.join(
907+
os.environ['GITHUB_WORKSPACE'],
908+
'release_homebrew_action',
909+
'test',
910+
)
911+
)
912+
913+
914+
def get_homebrew_temp_dir() -> str:
915+
"""
916+
Get the Homebrew temp root used by test-bot.
917+
918+
Returns
919+
-------
920+
str
921+
Absolute path to the Homebrew temp root.
922+
"""
923+
return os.path.abspath(
924+
os.path.join(
925+
tempfile.gettempdir(),
926+
'release_homebrew_action',
927+
)
928+
)
929+
930+
931+
def get_homebrew_test_artifacts_dir(formula: str) -> str:
932+
"""
933+
Get the formula temp directory where formulae can write during bottle builds.
934+
935+
Returns
936+
-------
937+
str
938+
Absolute path to the Homebrew-side test artifacts directory.
939+
"""
940+
return os.path.abspath(
941+
os.path.join(
942+
get_homebrew_temp_dir(),
943+
formula,
944+
'test',
945+
)
946+
)
947+
948+
949+
def prepare_test_artifacts_dir(formula: str) -> str:
950+
"""
951+
Prepare an action-owned directory for test artifacts from the bottle build.
952+
953+
Parameters
954+
----------
955+
formula : str
956+
Name of the Homebrew formula being validated.
957+
958+
Returns
959+
-------
960+
str
961+
Absolute path to the prepared test artifacts directory.
962+
"""
963+
test_artifacts_dir = get_test_artifacts_dir()
964+
action_work_dir = os.path.abspath(
965+
os.path.join(
966+
os.environ['GITHUB_WORKSPACE'],
967+
'release_homebrew_action',
968+
)
969+
)
970+
971+
if os.path.commonpath([action_work_dir, test_artifacts_dir]) != action_work_dir:
972+
raise ValueError(f'::error:: Refusing to clean unexpected path {test_artifacts_dir}')
973+
974+
if os.path.isdir(test_artifacts_dir):
975+
shutil.rmtree(test_artifacts_dir)
976+
elif os.path.exists(test_artifacts_dir):
977+
os.remove(test_artifacts_dir)
978+
979+
homebrew_test_artifacts_dir = get_homebrew_test_artifacts_dir(formula)
980+
homebrew_artifacts_root = os.path.abspath(
981+
os.path.join(
982+
get_homebrew_temp_dir(),
983+
formula,
984+
)
985+
)
986+
987+
if os.path.commonpath([homebrew_artifacts_root, homebrew_test_artifacts_dir]) != homebrew_artifacts_root:
988+
raise ValueError(f'::error:: Refusing to clean unexpected path {homebrew_test_artifacts_dir}')
989+
990+
if os.path.isdir(homebrew_test_artifacts_dir):
991+
shutil.rmtree(homebrew_test_artifacts_dir)
992+
elif os.path.exists(homebrew_test_artifacts_dir):
993+
os.remove(homebrew_test_artifacts_dir)
994+
995+
os.makedirs(test_artifacts_dir, exist_ok=True)
996+
os.makedirs(homebrew_test_artifacts_dir, exist_ok=True)
997+
998+
set_github_action_output(
999+
output_name='testpath',
1000+
output_value=test_artifacts_dir,
1001+
)
1002+
1003+
return test_artifacts_dir
1004+
1005+
1006+
def copy_test_artifacts(source_dir: str, destination_dir: str) -> None:
1007+
"""
1008+
Copy test artifacts from Homebrew logs to the action output directory.
1009+
1010+
Parameters
1011+
----------
1012+
source_dir : str
1013+
Directory where the formula wrote artifacts during the Homebrew build.
1014+
destination_dir : str
1015+
Directory exposed as the action's testpath output.
1016+
"""
1017+
if not os.path.isdir(source_dir):
1018+
print(f'No Homebrew test artifacts found at {source_dir}')
1019+
return
1020+
1021+
print(f'Copying Homebrew test artifacts from {source_dir} to {destination_dir}')
1022+
os.makedirs(destination_dir, exist_ok=True)
1023+
shutil.copytree(source_dir, destination_dir, dirs_exist_ok=True)
1024+
1025+
1026+
def brew_test_bot_only_formulae(formula: str, test_artifacts_dir: Optional[str] = None) -> bool:
8961027
start_group(f'Running brew test-bot --only-formulae for {formula}')
8971028

8981029
org_repo = os.environ['INPUT_ORG_HOMEBREW_REPO']
@@ -922,113 +1053,24 @@ def brew_test_bot_only_formulae(formula: str) -> bool:
9221053
args_list.append('--skip-livecheck')
9231054
print('Skipping livecheck (running from fork PR)')
9241055

925-
env = get_test_bot_env({
1056+
homebrew_temp_dir = get_homebrew_temp_dir()
1057+
homebrew_test_artifacts_dir = get_homebrew_test_artifacts_dir(formula)
1058+
extra_env = {
9261059
'HOMEBREW_BOTTLE_BUILD': 'true', # setting this will allow us to skip advanced tests when building bottles
1060+
HOMEBREW_TEMP_ENV_VAR: homebrew_temp_dir,
9271061
'HOMEBREW_NO_ASK': '1', # do not prompt for confirmation
928-
})
929-
930-
result = _run_subprocess(
931-
args_list=args_list,
932-
env=env,
933-
)
934-
935-
end_group()
936-
return result
937-
938-
939-
def find_tmp_dir(formula: str) -> str:
940-
print('Trying to find temp directory')
941-
root_tmp_dirs = [
942-
os.getenv('HOMEBREW_TEMP', ""), # if manually set
943-
'/private/tmp', # macOS default
944-
'/var/tmp', # Linux default
945-
]
946-
947-
# first tmp dir that exists
948-
root_tmp_dir = next((d for d in root_tmp_dirs if os.path.isdir(d)), None)
949-
950-
if not root_tmp_dir:
951-
raise FileNotFoundError('::error:: Could not find root temp directory')
952-
953-
print(f'Using temp directory {root_tmp_dir}')
954-
955-
# find formula temp directories not already in the list
956-
for d in os.listdir(root_tmp_dir):
957-
print(f'Checking temp directory {d}')
958-
tmp_dir = os.path.join(root_tmp_dir, d)
959-
if d.startswith(f'{formula}-') and tmp_dir not in TEMP_DIRECTORIES:
960-
print(f'Found temp directory {tmp_dir}')
961-
TEMP_DIRECTORIES.append(tmp_dir)
962-
break
963-
else:
964-
tmp_dir = ""
965-
966-
if not tmp_dir:
967-
raise FileNotFoundError(f'::error:: Could not find temp directory {tmp_dir}')
968-
969-
return tmp_dir
970-
971-
972-
def install_formula(formula: str) -> bool:
973-
start_group(f'Installing formula {formula}')
974-
975-
env = {
976-
'HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK': '1',
977-
}
978-
979-
# combine with os environment
980-
env.update(os.environ)
981-
982-
result = _run_subprocess(
983-
args_list=[
984-
'brew',
985-
'install',
986-
'--build-from-source',
987-
'--include-test',
988-
'--keep-tmp',
989-
'--verbose',
990-
f'{tap_repo_name}/{formula}',
991-
],
992-
env=env,
993-
)
994-
995-
global HOMEBREW_BUILDPATH
996-
HOMEBREW_BUILDPATH = find_tmp_dir(formula)
997-
998-
set_github_action_output(
999-
output_name='buildpath',
1000-
output_value=HOMEBREW_BUILDPATH
1001-
)
1002-
1003-
end_group()
1004-
return result
1005-
1006-
1007-
def test_formula(formula: str) -> bool:
1008-
start_group(f'Testing formula {formula}')
1009-
1010-
env = {
1011-
'HOMEBREW_BUILDPATH': HOMEBREW_BUILDPATH,
1062+
TEST_ARTIFACTS_ENV_VAR: homebrew_test_artifacts_dir,
10121063
}
10131064

1014-
# combine with os environment
1015-
env.update(os.environ)
1065+
env = get_test_bot_env(extra_env)
10161066

10171067
result = _run_subprocess(
1018-
args_list=[
1019-
'brew',
1020-
'test',
1021-
'--keep-tmp',
1022-
'--verbose',
1023-
f'{tap_repo_name}/{formula}',
1024-
],
1068+
args_list=args_list,
10251069
env=env,
10261070
)
10271071

1028-
set_github_action_output(
1029-
output_name='testpath',
1030-
output_value=find_tmp_dir(formula)
1031-
)
1072+
if test_artifacts_dir:
1073+
copy_test_artifacts(homebrew_test_artifacts_dir, test_artifacts_dir)
10321074

10331075
end_group()
10341076
return result
@@ -1042,9 +1084,11 @@ def main():
10421084
formula = process_input_formula(args.formula_file)
10431085

10441086
if os.environ['INPUT_VALIDATE'].lower() != 'true':
1045-
print('Skipping audit, install, and test')
1087+
print('Skipping audit and bottle validation')
10461088
return
10471089

1090+
test_artifacts_dir = prepare_test_artifacts_dir(formula)
1091+
10481092
if not brew_test_bot_only_cleanup_before():
10491093
print('::error:: brew test-bot --only-cleanup-before failed')
10501094
raise SystemExit(1)
@@ -1073,15 +1117,7 @@ def main():
10731117
print('::error:: brew test-bot --only-tap-syntax failed')
10741118
FAILURES.append('tap-syntax')
10751119

1076-
if not install_formula(formula):
1077-
print(f'::error:: Formula {formula} failed install')
1078-
FAILURES.append('install')
1079-
1080-
if not test_formula(formula):
1081-
print(f'::error:: Formula {formula} failed test')
1082-
FAILURES.append('test')
1083-
1084-
if not brew_test_bot_only_formulae(formula):
1120+
if not brew_test_bot_only_formulae(formula, test_artifacts_dir):
10851121
print('::error:: brew test-bot --only-formulae failed')
10861122
FAILURES.append('formulae')
10871123

0 commit comments

Comments
 (0)