feat: add approval step

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
2026-03-15 19:46:33 +01:00
parent 533b738692
commit 7eb6ae7cbb
41 changed files with 7886 additions and 1724 deletions

View File

@@ -144,6 +144,97 @@
</div>
{% endif %}
{# ── Policy evaluations (approval, soak, branch) ──────────── #}
{% if policy_evaluations | length > 0 %}
<div class="mb-8">
<h2 class="text-sm font-semibold text-gray-900 mb-3">Policy Requirements</h2>
<div class="border border-gray-200 rounded-lg divide-y divide-gray-100">
{% for eval in policy_evaluations %}
<div class="px-4 py-3">
<div class="flex items-center gap-3 text-sm">
{% if eval.passed %}
<svg class="w-4 h-4 text-green-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
{% else %}
<svg class="w-4 h-4 text-amber-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
{% endif %}
{% if eval.policy_type == "approval" %}
<span class="bg-emerald-100 text-emerald-700 text-xs font-medium px-1.5 py-0.5 rounded">Approval</span>
{% elif eval.policy_type == "soak_time" %}
<span class="bg-indigo-100 text-indigo-700 text-xs font-medium px-1.5 py-0.5 rounded">Soak Time</span>
{% elif eval.policy_type == "branch_restriction" %}
<span class="bg-orange-100 text-orange-700 text-xs font-medium px-1.5 py-0.5 rounded">Branch</span>
{% endif %}
<span class="text-gray-600">{{ eval.policy_name }}</span>
<span class="text-xs text-gray-400 ml-auto">{{ eval.reason }}</span>
</div>
{# ── Approval UI ──────────────────────────────── #}
{% if eval.policy_type == "approval" and eval.approval_state %}
<div class="mt-3 ml-7">
<div class="flex items-center gap-2 text-xs text-gray-500 mb-2">
<span class="font-medium">{{ eval.approval_state.current_approvals }}/{{ eval.approval_state.required_approvals }} approvals</span>
</div>
{% if eval.approval_state.decisions | length > 0 %}
<div class="space-y-1 mb-3">
{% for d in eval.approval_state.decisions %}
<div class="flex items-center gap-2 text-xs">
{% if d.decision == "approved" %}
<span class="w-1.5 h-1.5 rounded-full bg-green-500"></span>
<span class="text-green-700 font-medium">{{ d.username }}</span>
<span class="text-gray-400">approved</span>
{% else %}
<span class="w-1.5 h-1.5 rounded-full bg-red-500"></span>
<span class="text-red-700 font-medium">{{ d.username }}</span>
<span class="text-gray-400">rejected</span>
{% endif %}
{% if d.comment %}<span class="text-gray-400">— {{ d.comment }}</span>{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% if not eval.passed %}
{% if is_release_author and not is_admin %}
<p class="text-xs text-gray-500 italic">You cannot approve your own release.</p>
{% else %}
<div class="flex items-center gap-2">
{% if not is_release_author %}
<form method="post" action="/orgs/{{ org_name }}/projects/{{ project_name }}/releases/{{ artifact.slug }}/approve" class="inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="release_intent_id" value="{{ release_intent_id }}">
<input type="hidden" name="target_environment" value="{{ eval.target_environment }}">
<button type="submit" class="text-xs px-3 py-1.5 rounded-md bg-green-600 text-white hover:bg-green-700 transition-colors">Approve</button>
</form>
{% endif %}
{% if is_release_author and is_admin %}
<form method="post" action="/orgs/{{ org_name }}/projects/{{ project_name }}/releases/{{ artifact.slug }}/approve" class="inline" onsubmit="return confirm('You are the release author. This is an admin bypass — are you sure?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="release_intent_id" value="{{ release_intent_id }}">
<input type="hidden" name="target_environment" value="{{ eval.target_environment }}">
<input type="hidden" name="force_bypass" value="true">
<button type="submit" class="text-xs px-3 py-1.5 rounded-md bg-red-600 text-white hover:bg-red-700 transition-colors">Bypass (Admin)</button>
</form>
{% endif %}
<form method="post" action="/orgs/{{ org_name }}/projects/{{ project_name }}/releases/{{ artifact.slug }}/reject" class="inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="release_intent_id" value="{{ release_intent_id }}">
<input type="hidden" name="target_environment" value="{{ eval.target_environment }}">
<button type="submit" class="text-xs px-3 py-1.5 rounded-md border border-red-300 text-red-600 hover:bg-red-50 transition-colors">Reject</button>
</form>
</div>
{% endif %}
{% endif %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{# ── Destinations with status ──────────────────────────────── #}
{% if destinations | length > 0 or configured_destinations | length > 0 %}
<div class="mb-8">