diff --git a/skills/buildr-artifacts/SKILL.md b/skills/buildr-artifacts/SKILL.md index ecf39bc..976e0e5 100644 --- a/skills/buildr-artifacts/SKILL.md +++ b/skills/buildr-artifacts/SKILL.md @@ -1,7 +1,7 @@ --- name: buildr-artifacts description: Publish browser-viewable artifacts. Use static mode for S3-hosted HTML artifacts and Vite app mode for stateful apps that should run on Codexbox with a bld.run URL. -compatibility: Static mode requires aws CLI and Codexbox instance role permissions for the artifact S3 bucket. Vite app mode requires npm, tailscale, and an existing per-user bld.run DNS record. +compatibility: Static mode requires aws CLI and artifact S3 bucket credentials from ~/.env (ARTIFACT_AWS_*) or the Codexbox instance role. Vite app mode requires npm, tailscale, and an existing per-user bld.run DNS record. metadata: homepage: https://artifacts.buildrtools.com allowed-tools: Bash({baseDir}/scripts/share_artifact.py:*) Bash({baseDir}/scripts/serve_vite_app.py:*) Read @@ -92,7 +92,7 @@ Both scripts print only the shareable URL on success. Report that URL to the use ## Environment -Codexbox normally sources these from `/home/codex/.config/codexbox/env`, populated from Secrets Manager: +Static uploads read these from `~/.env` (dotenv format, optional `export` prefix). Variables already exported in the process environment take precedence over `~/.env`: - `ARTIFACT_AWS_REGION` — AWS region, falls back to `AWS_REGION`, then `us-east-1` - `ARTIFACT_AWS_ACCESS_KEY_ID` — optional artifact-scoped AWS access key for static uploads @@ -102,7 +102,7 @@ Codexbox normally sources these from `/home/codex/.config/codexbox/env`, populat - `ARTIFACT_BASE_URL` — URL base, defaults to `https://artifacts.buildrtools.com` - `BLD_RUN_HOST` — optional Vite app URL host override, for example `mike.bld.run` -Static uploads use the `ARTIFACT_AWS_*` values when provided; otherwise they rely on the Codexbox instance role or normal AWS CLI credential resolution. +Static uploads use the `ARTIFACT_AWS_*` values when provided; otherwise they rely on the Codexbox instance role or normal AWS CLI credential resolution. Vite app mode does not read `~/.env`; export `BLD_RUN_HOST` or pass `--url-host`. ## Safety Rules @@ -119,5 +119,5 @@ The script preserves the pi extension's constraints: - Prefer writing artifacts under `tmp/` or another scratch directory, then upload the final directory. - Do not upload secrets or raw customer data unless the user explicitly requests it and confirms the artifact is safe to share internally. -- If static upload fails with missing credentials or access denied, check that the Codexbox instance role has artifact bucket permissions. +- If static upload fails with missing credentials or access denied, check that `~/.env` defines `ARTIFACT_AWS_ACCESS_KEY_ID` and `ARTIFACT_AWS_SECRET_ACCESS_KEY` with artifact bucket permissions, or that the Codexbox instance role has them. - If Vite starts but the URL does not load, check the app logs in `~/.cache/buildr-artifacts/vite-apps//` and confirm the printed port is reachable over Tailscale. diff --git a/skills/buildr-artifacts/scripts/share_artifact.py b/skills/buildr-artifacts/scripts/share_artifact.py index 5325ced..e494e47 100755 --- a/skills/buildr-artifacts/scripts/share_artifact.py +++ b/skills/buildr-artifacts/scripts/share_artifact.py @@ -107,6 +107,7 @@ DEFAULT_BUCKET = "buildr-bizops-artifacts" DEFAULT_BASE_URL = "https://artifacts.buildrtools.com" +DOTENV_PATH = Path.home() / ".env" DEFAULT_MAX_FILES = 2000 DEFAULT_MAX_DEPTH = 20 MAX_FILE_BYTES = 1_073_741_824 @@ -123,6 +124,7 @@ class ArtifactFile: def main() -> int: args = parse_args() try: + load_dotenv(DOTENV_PATH) artifact_files = collect_input_files(args) slug = generate_slug() upload_files(artifact_files, slug) @@ -144,6 +146,28 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() +def load_dotenv(path: Path) -> None: + """Fill missing environment variables from a dotenv file. Exported values win.""" + if not path.is_file(): + return + + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + + name, value = line.split("=", 1) + name = name.removeprefix("export ").strip() + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"": + value = value[1:-1] + else: + value = value.split(" #", 1)[0].rstrip() + + if name and name not in os.environ: + os.environ[name] = value + + def collect_input_files(args: argparse.Namespace) -> list[ArtifactFile]: if args.html is not None: return [inline_html_file(args.html)]