ci(release): build image for all branches, not just feature/** (#19)

* dryrun: intentional failing test (will be reverted)

* feat(auth): optional SEED_ADMIN_PASSWORD to pre-hash seed admin (dev helper)

Terraform gains enable_seed_password + seed_admin_password (sensitive) vars
on the customer-instance module; when enabled the password is piped via
startup-script into /opt/agnes/.env as SEED_ADMIN_PASSWORD. On first boot
app/main.py argon2-hashes it onto the seed user so the admin can log in
immediately without going through /auth/bootstrap. Never overwrites an
existing password_hash — safe against accidental reset on terraform apply.

* ci(release): build :dev-<slug> on any branch, not just feature/**

Before: only 'feature/**' branches triggered release.yml, so pushing
'zs/my-edit' or 'fix/bug' did not publish an image. dev_instances entry
pinning image_tag = 'dev-zs-my-edit' then crashed VM startup with
'image not found'.

Now: any branch push (except main, which produces :stable) publishes
:dev-<slug>. Slug strips a leading 'feature/' and replaces non-[a-z0-9-]
with '-', keeping existing feature/** behavior identical.

* Revert "dryrun: intentional failing test (will be reverted)"

This reverts commit cf9cc06a7884bb401ff29fc5cb6d8baf84dc3daa.
This commit is contained in:
ZdenekSrotyr 2026-04-21 21:33:57 +02:00 committed by GitHub
parent 1ca5295d54
commit e2eb51f657
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 57 additions and 19 deletions

View file

@ -2,7 +2,9 @@ name: Release
on:
push:
branches: [main, "feature/**"]
branches:
- main
- "**" # build :dev-<slug> image for any branch push (e.g. feature/x, zs/edit, fix/y)
permissions:
contents: write

View file

@ -90,7 +90,10 @@ def create_app() -> FastAPI:
SCHEMA_VERSION,
)
# Seed admin user for testing/CI (when SEED_ADMIN_EMAIL is set)
# Seed admin user for testing/CI (when SEED_ADMIN_EMAIL is set).
# Optional: SEED_ADMIN_PASSWORD sets password_hash on first seed so the user
# can log in immediately without bootstrap. Only applied if the user has no
# password_hash yet — never overwrites an existing password.
seed_email = os.environ.get("SEED_ADMIN_EMAIL")
if seed_email:
try:
@ -98,10 +101,25 @@ def create_app() -> FastAPI:
from src.repositories.users import UserRepository
conn = get_system_db()
repo = UserRepository(conn)
if not repo.get_by_email(seed_email):
seed_password = os.environ.get("SEED_ADMIN_PASSWORD") or None
password_hash = None
if seed_password:
from argon2 import PasswordHasher
password_hash = PasswordHasher().hash(seed_password)
existing = repo.get_by_email(seed_email)
if not existing:
import uuid
repo.create(id=str(uuid.uuid4()), email=seed_email, name="Admin", role="admin")
logger.info("Seeded admin user: %s", seed_email)
repo.create(
id=str(uuid.uuid4()),
email=seed_email,
name="Admin",
role="admin",
password_hash=password_hash,
)
logger.info("Seeded admin user: %s (password=%s)", seed_email, "yes" if password_hash else "no")
elif password_hash and not existing.get("password_hash"):
repo.update(id=existing["id"], password_hash=password_hash, role="admin")
logger.info("Set password on existing seed admin: %s", seed_email)
conn.close()
except Exception as e:
logger.warning(f"Could not seed admin: {e}")

View file

@ -21,6 +21,8 @@ SESSION_SECRET= # python -c "import secrets; print(secrets.token_he
# ── BOOTSTRAP (first deploy only) ───────────────────
# SEED_ADMIN_EMAIL=admin@example.com
# SEED_ADMIN_PASSWORD= # Dev helper only — sets password_hash on seed.
# # Never overwrites an existing password.
# ── EMAIL / SMTP (required for magic link auth) ─────
# SMTP_HOST=smtp.gmail.com

View file

@ -209,18 +209,19 @@ resource "google_compute_instance" "vm" {
}
metadata_startup_script = templatefile("${path.module}/startup-script.sh.tpl", {
customer_name = var.customer_name
image_repo = var.image_repo
image_tag = each.value.image_tag
upgrade_mode = each.value.upgrade_mode
tls_mode = each.value.tls_mode
domain = each.value.domain
acme_email = var.acme_email != "" ? var.acme_email : var.seed_admin_email
data_source = var.data_source
keboola_stack_url = var.keboola_stack_url
seed_admin_email = var.seed_admin_email
role = each.value.role
compose_ref = var.compose_ref
customer_name = var.customer_name
image_repo = var.image_repo
image_tag = each.value.image_tag
upgrade_mode = each.value.upgrade_mode
tls_mode = each.value.tls_mode
domain = each.value.domain
acme_email = var.acme_email != "" ? var.acme_email : var.seed_admin_email
data_source = var.data_source
keboola_stack_url = var.keboola_stack_url
seed_admin_email = var.seed_admin_email
seed_admin_password = var.enable_seed_password ? var.seed_admin_password : ""
role = each.value.role
compose_ref = var.compose_ref
})
service_account {
@ -289,8 +290,8 @@ resource "google_monitoring_alert_policy" "health_failure" {
conditions {
display_name = "Uptime check failed > 5 min"
condition_threshold {
filter = "metric.type=\"monitoring.googleapis.com/uptime_check/check_passed\" AND metric.labels.check_id=\"${google_monitoring_uptime_check_config.health[each.key].uptime_check_id}\" AND resource.type=\"uptime_url\""
duration = "300s"
filter = "metric.type=\"monitoring.googleapis.com/uptime_check/check_passed\" AND metric.labels.check_id=\"${google_monitoring_uptime_check_config.health[each.key].uptime_check_id}\" AND resource.type=\"uptime_url\""
duration = "300s"
# ALIGN_FRACTION_TRUE yields fraction of checks that returned true.
# If the fraction stays < 1 (i.e. any probe failed) for 5 min alert.
comparison = "COMPARISON_LT"

View file

@ -15,6 +15,7 @@ ACME_EMAIL="${acme_email}"
DATA_SOURCE="${data_source}"
KEBOOLA_STACK_URL="${keboola_stack_url}"
SEED_ADMIN_EMAIL="${seed_admin_email}"
SEED_ADMIN_PASSWORD="${seed_admin_password}"
ROLE="${role}"
COMPOSE_REF="${compose_ref}"
@ -81,6 +82,7 @@ DATA_SOURCE=$DATA_SOURCE
KEBOOLA_STORAGE_TOKEN=$KEBOOLA_TOKEN
KEBOOLA_STACK_URL=$KEBOOLA_STACK_URL
SEED_ADMIN_EMAIL=$SEED_ADMIN_EMAIL
SEED_ADMIN_PASSWORD=$SEED_ADMIN_PASSWORD
LOG_LEVEL=info
DOMAIN=$DOMAIN
AGNES_TAG=$IMAGE_TAG

View file

@ -53,6 +53,19 @@ variable "seed_admin_email" {
type = string
}
variable "enable_seed_password" {
description = "Pokud true, seed admin user dostane hned password_hash ze seed_admin_password (dev helper). Ponech false v prod — admin si heslo nastaví přes /auth/bootstrap nebo Google OAuth."
type = bool
default = false
}
variable "seed_admin_password" {
description = "Plain-text heslo pro seed admina. Použije se jen když enable_seed_password=true. POZOR: ukládá se do Terraform state."
type = string
default = ""
sensitive = true
}
variable "data_source" {
description = "Typ data source — keboola | bigquery | csv"
type = string