diff --git a/.github/scripts/prepare-deploy.py b/.github/scripts/prepare-deploy.py index a874b77..56affd0 100644 --- a/.github/scripts/prepare-deploy.py +++ b/.github/scripts/prepare-deploy.py @@ -4,6 +4,7 @@ import json import os from pathlib import Path +import re import subprocess import sys @@ -15,6 +16,16 @@ def gcloud(*args): return result.stdout +def failure_message(error): + operation = " ".join(error.cmd[1:4]) + categories = re.findall( + r"\b(?:PERMISSION_DENIED|NOT_FOUND|INVALID_ARGUMENT|UNAUTHENTICATED|FAILED_PRECONDITION|RESOURCE_EXHAUSTED)\b", + error.stderr or "", + ) + category = categories[0] if categories else "unclassified CLI error" + return f"gcloud {operation} failed (exit {error.returncode}, {category}); command output withheld." + + def prepare(service, image, config_names, read_secret): # Export only writable metadata; keep the service's runtime configuration. manifest = { @@ -110,4 +121,4 @@ def read_secret(name, version): main() except subprocess.CalledProcessError as error: # Never print captured command output, which may contain configuration. - sys.exit(f"gcloud failed (exit {error.returncode}); no secret values were logged.") + sys.exit(failure_message(error)) diff --git a/.github/scripts/test_prepare_deploy.py b/.github/scripts/test_prepare_deploy.py index d8831d0..3ab95ce 100644 --- a/.github/scripts/test_prepare_deploy.py +++ b/.github/scripts/test_prepare_deploy.py @@ -3,6 +3,7 @@ import json import os from pathlib import Path +import subprocess import tempfile import unittest from unittest.mock import Mock, patch @@ -95,6 +96,14 @@ def test_secret_access_failure_prevents_any_deployment(self): self.assertEqual(cli.call_count, 2) self.assertFalse((Path(directory) / "cloud-run-service.json").exists()) + def test_failure_diagnostics_do_not_expose_command_arguments_or_output(self): + error = subprocess.CalledProcessError(1, ["gcloud", "secrets", "versions", "access", "latest", "--secret", "private-name"], + output="private-value", stderr="PERMISSION_DENIED private-value") + message = module.failure_message(error) + self.assertIn("secrets versions access", message) + self.assertIn("PERMISSION_DENIED", message) + self.assertNotIn("private", message) + if __name__ == "__main__": unittest.main()