Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/actions/export-env-vars/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ runs:
vars_map = json.loads(os.environ.get("VARS_JSON", "{}"))
env_path = os.environ["GITHUB_ENV"]
alias_prefixes = (
"BB_RPC_URL_HTTP_",
"BB_RPC_URL_WS_",
"BB_DEV_RPC_URL_HTTP_",
"BB_DEV_RPC_URL_WS_",
"BB_PROD_RPC_URL_HTTP_",
"BB_PROD_RPC_URL_WS_",
"BB_RPC_BIND_HOST_",
"BB_RPC_ALLOW_IP_",
"BB_TEST_API_URL_HTTP_",
"BB_TEST_API_URL_WS_",
"BB_DEV_API_URL_HTTP_",
"BB_DEV_API_URL_WS_",
)

def write_env_var(env_file, key, value):
Expand Down
47 changes: 27 additions & 20 deletions .github/scripts/build_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
LOG_PREFIX = "CI/CD Pipeline:"
SCRIPT_NAME = "[build-packages]"
DEFAULT_PACKAGE_ROOT = "/opt/blockbook-builds"
BUILD_ENV_VAR = "BB_BUILD_ENV"
BUILD_ENV_DEV = "dev"
BUILD_ENV_PROD = "prod"


def log(message: str) -> None:
Expand Down Expand Up @@ -50,17 +53,19 @@ def get_coin_alias(config: dict, coin: str) -> str:
return value.strip().lower()


def resolve_backend_domain(always_build_backend: bool) -> str:
domain = os.environ.get("BB_BACKEND_DOMAIN", "").strip()
if always_build_backend:
return domain
if not domain:
fail("BB_BACKEND_DOMAIN must be set unless --always-build-backend is used")
return domain
def resolve_build_env() -> str:
build_env = os.environ.get(BUILD_ENV_VAR, "").strip().lower()
if not build_env:
return BUILD_ENV_DEV
if build_env in {BUILD_ENV_DEV, BUILD_ENV_PROD}:
return build_env
fail(f"invalid {BUILD_ENV_VAR} value '{build_env}', expected 'dev' or 'prod'")
return ""


def rpc_url_env_name(alias: str) -> str:
return f"BB_RPC_URL_HTTP_{alias}"
def rpc_url_env_name(alias: str, build_env: str) -> str:
prefix = "BB_DEV_RPC_URL_HTTP_" if build_env == BUILD_ENV_DEV else "BB_PROD_RPC_URL_HTTP_"
return f"{prefix}{alias}"


def rpc_hostname(url: str) -> str:
Expand All @@ -75,16 +80,18 @@ def rpc_hostname(url: str) -> str:
def should_build_backend(
*,
always_build_backend: bool,
backend_domain: str,
rpc_host: str,
rpc_url: str,
) -> tuple[bool, str]:
if always_build_backend:
return True, "always-build-backend"
if backend_domain and backend_domain == rpc_host:
return True, f"rpc-host-matches-{backend_domain}"
if not rpc_url:
return True, "rpc-url-env-missing-or-empty"
rpc_host = rpc_hostname(rpc_url)
if not rpc_host:
return False, "rpc-host-missing"
return False, f"rpc-host-does-not-match-{backend_domain}"
if rpc_host in {"localhost", "127.0.0.1", "::1"}:
return True, f"rpc-host-is-local-{rpc_host}"
return False, f"rpc-host-is-remote-{rpc_host}"


def resolve_branch_or_tag() -> str:
Expand Down Expand Up @@ -143,7 +150,7 @@ def main(argv: list[str] | None = None) -> None:
args = parsed.coins

always_build_backend = parsed.always_build_backend
backend_domain = resolve_backend_domain(always_build_backend)
build_env = resolve_build_env()

package_root = os.environ.get("BB_PACKAGE_ROOT", "").strip() or DEFAULT_PACKAGE_ROOT
if not os.path.isabs(package_root):
Expand All @@ -153,7 +160,8 @@ def main(argv: list[str] | None = None) -> None:

log("requested coins: " + " ".join(args))
log(f"always_build_backend={int(always_build_backend)}")
log(f"BB_BACKEND_DOMAIN={backend_domain or '<unset>'}")
log(f"{BUILD_ENV_VAR}={build_env}")
log("backend build rule: build unless the selected BB_{DEV|PROD}_RPC_URL_HTTP is non-empty and non-local")
log(f"branch_or_tag={branch_or_tag} -> path={branch_or_tag_path}")
log(f"package_root={package_root}")

Expand All @@ -172,14 +180,13 @@ def main(argv: list[str] | None = None) -> None:
blockbook_package_name = get_package_name(config, "blockbook", coin)
backend_package_name = get_package_name(config, "backend", coin)
coin_alias = get_coin_alias(config, coin)
rpc_env = rpc_url_env_name(coin_alias)
rpc_env = rpc_url_env_name(coin_alias, build_env)
rpc_url = os.environ.get(rpc_env, "").strip()
host = rpc_hostname(rpc_url)
build_backend, reason = should_build_backend(
always_build_backend=always_build_backend,
backend_domain=backend_domain,
rpc_host=host,
rpc_url=rpc_url,
)
host = rpc_hostname(rpc_url)

coins.append(coin)
blockbook_package_names.append(blockbook_package_name)
Expand Down
136 changes: 119 additions & 17 deletions .github/scripts/build_packages_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ def run_build(
self,
*,
coin: str,
rpc_env: str,
rpc_url: str,
build_env: str | None = None,
rpc_env: str | None = None,
rpc_url: str | None = None,
always_build_backend: bool,
) -> tuple[list[str], str]:
commands: list[list[str]] = []
Expand Down Expand Up @@ -76,14 +77,16 @@ def fake_run(cmd, check, **kwargs):
env = {
"BRANCH_OR_TAG": "feature/test-branch",
"BB_PACKAGE_ROOT": str(self.package_root),
"BB_BACKEND_DOMAIN": "backend.example.test",
rpc_env: rpc_url,
}
if build_env is not None:
env["BB_BUILD_ENV"] = build_env
if rpc_env is not None and rpc_url is not None:
env[rpc_env] = rpc_url
stdout = io.StringIO()
old_cwd = Path.cwd()
try:
os.chdir(self.workspace)
with patch.dict(os.environ, env, clear=False), patch("build_packages.subprocess.run", side_effect=fake_run):
with patch.dict(os.environ, env, clear=True), patch("build_packages.subprocess.run", side_effect=fake_run):
with contextlib.redirect_stdout(stdout):
argv = [coin]
if always_build_backend:
Expand All @@ -94,11 +97,11 @@ def fake_run(cmd, check, **kwargs):

return commands[-1], stdout.getvalue().strip()

def test_builds_backend_when_rpc_url_matches_backend_domain(self) -> None:
def test_builds_backend_when_rpc_url_uses_localhost(self) -> None:
make_cmd, output = self.run_build(
coin="base_archive",
rpc_env="BB_RPC_URL_HTTP_base_archive",
rpc_url="http://backend.example.test:18026",
rpc_env="BB_DEV_RPC_URL_HTTP_base_archive",
rpc_url="http://localhost:18026",
always_build_backend=False,
)

Expand All @@ -108,10 +111,24 @@ def test_builds_backend_when_rpc_url_matches_backend_domain(self) -> None:
self.assertTrue((staged_dir / "blockbook-base_1.0_amd64.deb").is_file())
self.assertTrue((staged_dir / "backend-base_1.0_amd64.deb").is_file())

def test_skips_backend_when_rpc_url_does_not_match_backend_domain(self) -> None:
def test_builds_backend_when_rpc_url_uses_loopback_ip(self) -> None:
make_cmd, output = self.run_build(
coin="base_archive",
rpc_env="BB_RPC_URL_HTTP_base_archive",
rpc_env="BB_DEV_RPC_URL_HTTP_base_archive",
rpc_url="http://127.0.0.1:18026",
always_build_backend=False,
)

self.assertEqual(make_cmd, ["make", "deb-base_archive"])
self.assertEqual(output, "build/blockbook-base_1.0_amd64.deb")
staged_dir = self.package_root / "feature-test-branch" / "base_archive"
self.assertTrue((staged_dir / "blockbook-base_1.0_amd64.deb").is_file())
self.assertTrue((staged_dir / "backend-base_1.0_amd64.deb").is_file())

def test_skips_backend_when_rpc_url_host_is_remote(self) -> None:
make_cmd, output = self.run_build(
coin="base_archive",
rpc_env="BB_DEV_RPC_URL_HTTP_base_archive",
rpc_url="https://rpc.example.invalid/",
always_build_backend=False,
)
Expand All @@ -122,11 +139,51 @@ def test_skips_backend_when_rpc_url_does_not_match_backend_domain(self) -> None:
self.assertTrue((staged_dir / "blockbook-base_1.0_amd64.deb").is_file())
self.assertFalse((staged_dir / "backend-base_1.0_amd64.deb").exists())

def test_skips_backend_when_domain_only_appears_in_rpc_path(self) -> None:
def test_skips_backend_when_localhost_only_appears_in_rpc_path(self) -> None:
make_cmd, output = self.run_build(
coin="base_archive",
rpc_env="BB_DEV_RPC_URL_HTTP_base_archive",
rpc_url="https://rpc.example.invalid/localhost",
always_build_backend=False,
)

self.assertEqual(make_cmd, ["make", "deb-blockbook-base_archive"])
self.assertEqual(output, "build/blockbook-base_1.0_amd64.deb")
staged_dir = self.package_root / "feature-test-branch" / "base_archive"
self.assertTrue((staged_dir / "blockbook-base_1.0_amd64.deb").is_file())
self.assertFalse((staged_dir / "backend-base_1.0_amd64.deb").exists())

def test_builds_backend_when_rpc_url_env_is_missing(self) -> None:
make_cmd, output = self.run_build(
coin="base_archive",
always_build_backend=False,
)

self.assertEqual(make_cmd, ["make", "deb-base_archive"])
self.assertEqual(output, "build/blockbook-base_1.0_amd64.deb")
staged_dir = self.package_root / "feature-test-branch" / "base_archive"
self.assertTrue((staged_dir / "blockbook-base_1.0_amd64.deb").is_file())
self.assertTrue((staged_dir / "backend-base_1.0_amd64.deb").is_file())

def test_builds_backend_when_rpc_url_env_is_empty(self) -> None:
make_cmd, output = self.run_build(
coin="base_archive",
rpc_env="BB_DEV_RPC_URL_HTTP_base_archive",
rpc_url="",
always_build_backend=False,
)

self.assertEqual(make_cmd, ["make", "deb-base_archive"])
self.assertEqual(output, "build/blockbook-base_1.0_amd64.deb")
staged_dir = self.package_root / "feature-test-branch" / "base_archive"
self.assertTrue((staged_dir / "blockbook-base_1.0_amd64.deb").is_file())
self.assertTrue((staged_dir / "backend-base_1.0_amd64.deb").is_file())

def test_skips_backend_when_rpc_url_env_is_non_empty_but_invalid(self) -> None:
make_cmd, output = self.run_build(
coin="base_archive",
rpc_env="BB_RPC_URL_HTTP_base_archive",
rpc_url="https://rpc.example.invalid/backend.example.test",
rpc_env="BB_DEV_RPC_URL_HTTP_base_archive",
rpc_url="not-a-loopback-url",
always_build_backend=False,
)

Expand All @@ -136,10 +193,10 @@ def test_skips_backend_when_domain_only_appears_in_rpc_path(self) -> None:
self.assertTrue((staged_dir / "blockbook-base_1.0_amd64.deb").is_file())
self.assertFalse((staged_dir / "backend-base_1.0_amd64.deb").exists())

def test_always_build_backend_overrides_domain_matching(self) -> None:
def test_always_build_backend_overrides_localhost_detection(self) -> None:
make_cmd, output = self.run_build(
coin="base_archive",
rpc_env="BB_RPC_URL_HTTP_base_archive",
rpc_env="BB_DEV_RPC_URL_HTTP_base_archive",
rpc_url="https://rpc.example.invalid/",
always_build_backend=True,
)
Expand All @@ -152,8 +209,8 @@ def test_always_build_backend_overrides_domain_matching(self) -> None:
def test_staging_uses_config_name_while_rpc_env_uses_alias(self) -> None:
make_cmd, output = self.run_build(
coin="polygon_archive",
rpc_env="BB_RPC_URL_HTTP_polygon_archive_bor",
rpc_url="http://backend.example.test:8545",
rpc_env="BB_DEV_RPC_URL_HTTP_polygon_archive_bor",
rpc_url="http://localhost:8545",
always_build_backend=False,
)

Expand All @@ -165,6 +222,51 @@ def test_staging_uses_config_name_while_rpc_env_uses_alias(self) -> None:
self.assertTrue((staged_dir / "backend-polygon_1.0_amd64.deb").is_file())
self.assertFalse(alias_dir.exists())

def test_prod_build_env_uses_prod_rpc_url_prefix(self) -> None:
make_cmd, output = self.run_build(
coin="base_archive",
build_env="prod",
rpc_env="BB_PROD_RPC_URL_HTTP_base_archive",
rpc_url="https://rpc.example.invalid/",
always_build_backend=False,
)

self.assertEqual(make_cmd, ["make", "deb-blockbook-base_archive"])
self.assertEqual(output, "build/blockbook-base_1.0_amd64.deb")
staged_dir = self.package_root / "feature-test-branch" / "base_archive"
self.assertTrue((staged_dir / "blockbook-base_1.0_amd64.deb").is_file())
self.assertFalse((staged_dir / "backend-base_1.0_amd64.deb").exists())

def test_prod_build_env_ignores_dev_rpc_url_prefix(self) -> None:
make_cmd, output = self.run_build(
coin="base_archive",
build_env="prod",
rpc_env="BB_DEV_RPC_URL_HTTP_base_archive",
rpc_url="https://rpc.example.invalid/",
always_build_backend=False,
)

self.assertEqual(make_cmd, ["make", "deb-base_archive"])
self.assertEqual(output, "build/blockbook-base_1.0_amd64.deb")
staged_dir = self.package_root / "feature-test-branch" / "base_archive"
self.assertTrue((staged_dir / "blockbook-base_1.0_amd64.deb").is_file())
self.assertTrue((staged_dir / "backend-base_1.0_amd64.deb").is_file())

def test_fails_on_invalid_build_env(self) -> None:
env = {
"BRANCH_OR_TAG": "feature/test-branch",
"BB_PACKAGE_ROOT": str(self.package_root),
"BB_BUILD_ENV": "staging",
}
old_cwd = Path.cwd()
try:
os.chdir(self.workspace)
with patch.dict(os.environ, env, clear=True), patch("build_packages.subprocess.run"):
with self.assertRaises(SystemExit):
build_packages.main(["base_archive"])
finally:
os.chdir(old_cwd)


if __name__ == "__main__":
unittest.main()
7 changes: 6 additions & 1 deletion .github/scripts/build_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from runner import (
PRODUCTION_RUNNER,
ValidationError,
build_runner_labels,
fail,
load_coin_context,
log,
Expand Down Expand Up @@ -44,6 +45,7 @@ def main() -> None:
"runner": runner,
"coins": coins,
"coins_csv": ",".join(coins),
"labels_json": json.dumps(build_runner_labels(runner, build_env), separators=(",", ":")),
}
)

Expand All @@ -60,7 +62,10 @@ def main() -> None:
log("Skipped prod-only coins for env=dev: " + ", ".join(selection.skipped_prod_only))
log("Selected coins: " + ", ".join(selection.coins))
for item in runner_matrix:
log(f"Runner {item['runner']}: {', '.join(item['coins'])}")
log(
f"Runner {item['runner']} labels={item['labels_json']}: "
+ ", ".join(item["coins"])
)


if __name__ == "__main__":
Expand Down
Loading
Loading