Compare commits

...
2 Commits
Author SHA1 Message Date
gitea-actions 98f7aa8a2d chore(release): v1.0.5
Build and Push Images / Build jorgecuadros-web (push) Canceled after 0s
Build and Push Images / Build jorgecuadros-api (push) Canceled after 0s
Cut by rmancinas via the "Cut release" workflow. Pushing the tag triggers build.yml; deploy separately with tag=1.0.5.
2026-08-01 09:22:56 +00:00
rmancinasandClaude Opus 5 898cf48c80 fix(migration): re-import died on the last step because blob_extract required deploy/.env.prod
Every transform resolves its target through dbenv.database_url(), which lets a
DATABASE_URL in the process environment win — that is how the API container
drives a re-import against its own database with no deploy/ directory present.
blob_extract.py was the one step that bypassed it and called load_env()
directly for the MinIO credentials, so the "Operaciones" re-import loaded all
the data and then exited 1 on:

  missing /repo/deploy/.env.prod — deploy the 'prod' DB stack and write its
  .env first

Give the S3 settings the same resolution as the DB URL: load_env() now returns
{} for an absent file, and setting()/require() layer the process environment on
top of it. blob_extract reads S3_ENDPOINT / S3_BUCKET and accepts either
S3_ACCESS_KEY/S3_SECRET_KEY or MINIO_ROOT_USER/MINIO_ROOT_PASSWORD, matching
the fallback order in storage.service.ts and the vars the api service already
sets in deploy/galactus/jorgecuadros-app.compose.yml. A genuinely missing
setting still fails fast, now naming the variable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 02:21:38 -07:00
6 changed files with 44 additions and 16 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@jorgecuadros/api", "name": "@jorgecuadros/api",
"version": "1.0.4", "version": "1.0.5",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "nest build", "build": "nest build",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@jorgecuadros/web", "name": "@jorgecuadros/web",
"version": "1.0.4", "version": "1.0.5",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 4500", "dev": "next dev -p 4500",
+9 -5
View File
@@ -33,7 +33,7 @@ from pathlib import Path
import boto3 import boto3
from botocore.config import Config from botocore.config import Config
from dbenv import connect, load_env from dbenv import connect, require, setting
from extract import sanitize_column_name as san from extract import sanitize_column_name as san
csv.field_size_limit(300_000_000) csv.field_size_limit(300_000_000)
@@ -102,12 +102,16 @@ def main():
only = set(x.strip() for x in args.tables.split(",") if x.strip()) only = set(x.strip() for x in args.tables.split(",") if x.strip())
sources = [s for s in SOURCES if not only or s["key"] in only] sources = [s for s in SOURCES if not only or s["key"] in only]
env = load_env(args.env) # Process environment first, deploy/.env.<env> second, with the same
# credential aliases the API uses — the "Operaciones" re-import runs this
# inside the API container, which has S3_ENDPOINT / MINIO_ROOT_* injected
# and no deploy/ directory at all.
s3 = boto3.client( s3 = boto3.client(
"s3", endpoint_url=env["S3_ENDPOINT"], "s3", endpoint_url=require(args.env, "S3_ENDPOINT"),
aws_access_key_id=env["MINIO_ROOT_USER"], aws_secret_access_key=env["MINIO_ROOT_PASSWORD"], aws_access_key_id=require(args.env, "S3_ACCESS_KEY", "MINIO_ROOT_USER"),
aws_secret_access_key=require(args.env, "S3_SECRET_KEY", "MINIO_ROOT_PASSWORD"),
config=Config(signature_version="s3v4"), region_name="us-east-1") config=Config(signature_version="s3v4"), region_name="us-east-1")
bucket = env["S3_BUCKET"] bucket = setting(args.env, "S3_BUCKET") or "jorgecuadros-documents"
conn = connect(args.env) conn = connect(args.env)
cur = conn.cursor() cur = conn.cursor()
+31 -7
View File
@@ -32,29 +32,53 @@ REPO = Path(__file__).resolve().parents[1]
def load_env(env: str) -> dict: def load_env(env: str) -> dict:
"""deploy/.env.<env> parsed to a dict, or {} when the file is absent.
Absent is normal, not an error: the API container runs these scripts with
DATABASE_URL / S3_* injected as real environment variables and ships no
deploy/ directory. Use `setting()` / `require()` rather than this — they
layer the process environment on top, which is what actually resolves."""
f = REPO / "deploy" / f".env.{env}" f = REPO / "deploy" / f".env.{env}"
if not f.exists(): if not f.exists():
raise SystemExit( return {}
f"missing {f} — deploy the '{env}' DB stack and write its .env first "
f"(see dbenv.py header)."
)
out = {} out = {}
for line in f.read_text().splitlines(): for line in f.read_text().splitlines():
line = line.strip() line = line.strip()
if line and not line.startswith("#") and "=" in line: if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1) k, v = line.split("=", 1)
out[k] = v out[k] = v
if "DATABASE_URL" not in out:
raise SystemExit(f"{f} has no DATABASE_URL")
return out return out
def setting(env: str, *keys: str):
"""First non-empty value for `keys`, process environment first, then
deploy/.env.<env>. Several keys = fallback aliases (S3_ACCESS_KEY then
MINIO_ROOT_USER, as apps/api/src/storage/storage.service.ts does)."""
fromfile = load_env(env)
for k in keys:
v = os.environ.get(k) or fromfile.get(k)
if v:
return v
return None
def require(env: str, *keys: str) -> str:
v = setting(env, *keys)
if not v:
raise SystemExit(
f"missing {' / '.join(keys)} — set it in the environment, or deploy the "
f"'{env}' stack and write {REPO / 'deploy' / f'.env.{env}'} "
f"(see dbenv.py header)."
)
return v
def database_url(env: str) -> str: def database_url(env: str) -> str:
"""Target DB URL. A DATABASE_URL in the process environment wins over """Target DB URL. A DATABASE_URL in the process environment wins over
deploy/.env.<env> — this is how the API container (which has its own deploy/.env.<env> — this is how the API container (which has its own
DATABASE_URL and no deploy/.env files) drives a re-import against its own DATABASE_URL and no deploy/.env files) drives a re-import against its own
database.""" database."""
return os.environ.get("DATABASE_URL") or load_env(env)["DATABASE_URL"] return require(env, "DATABASE_URL")
def connect(env: str): def connect(env: str):
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jorgecuadros-platform", "name": "jorgecuadros-platform",
"version": "1.0.4", "version": "1.0.5",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@jorgecuadros/database", "name": "@jorgecuadros/database",
"version": "1.0.4", "version": "1.0.5",
"private": true, "private": true,
"main": "generated/client/index.js", "main": "generated/client/index.js",
"types": "generated/client/index.d.ts", "types": "generated/client/index.d.ts",