@@ -79,6 +79,49 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Deploy action ─────────────────────────────────────────── #}
|
||||
{% if is_admin %}
|
||||
<div class="mb-8">
|
||||
<details class="border border-gray-200 rounded-lg group">
|
||||
<summary class="px-4 py-3 flex items-center gap-2 text-sm cursor-pointer list-none hover:bg-gray-50">
|
||||
<svg class="w-4 h-4 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||||
<span class="font-medium text-gray-700">Deploy this release</span>
|
||||
<svg class="w-3 h-3 text-gray-400 ml-auto transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
||||
</summary>
|
||||
<div class="px-4 py-4 border-t border-gray-100">
|
||||
<form method="post" action="/orgs/{{ org_name }}/projects/{{ project_name }}/deploy" class="space-y-4">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="artifact_id" value="{{ artifact_id }}">
|
||||
|
||||
{% if has_active_pipeline %}
|
||||
<div class="flex items-center gap-3 p-3 bg-purple-50 border border-purple-200 rounded-md">
|
||||
<input type="checkbox" id="use-pipeline" name="use_pipeline" value="true" checked class="rounded border-gray-300 text-purple-600 focus:ring-purple-500">
|
||||
<label for="use-pipeline" class="text-sm text-purple-800">
|
||||
Use pipeline <span class="text-purple-600 text-xs">(follows the configured multi-stage deployment pipeline)</span>
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="env-select">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Target environment</label>
|
||||
<select name="environment" class="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-gray-900">
|
||||
<option value="">All environments (pipeline decides)</option>
|
||||
{% for env in environments %}
|
||||
<option value="{{ env.name }}">{{ env.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="text-xs text-gray-500 mt-1">Leave empty when using a pipeline — it will deploy to all configured stages.</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-gray-900 text-white px-4 py-2 rounded-md text-sm hover:bg-gray-800 transition-colors">
|
||||
Deploy
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Pipeline stages ───────────────────────────────────────── #}
|
||||
{% if has_pipeline and pipeline_stages | length > 0 %}
|
||||
<div class="mb-8">
|
||||
@@ -95,6 +138,8 @@
|
||||
<svg class="w-4 h-4 text-blue-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
{% elif stage.status == "FAILED" %}
|
||||
<svg class="w-4 h-4 text-red-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
{% elif stage.status == "AWAITING_APPROVAL" or (stage.stage_type == "plan" and (stage.approval_status == "AWAITINGAPPROVAL" or stage.approval_status == "AWAITING_APPROVAL")) %}
|
||||
<svg class="w-4 h-4 text-purple-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-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>
|
||||
{% else %}
|
||||
<svg class="w-4 h-4 text-gray-300 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" stroke-width="2"/></svg>
|
||||
{% endif %}
|
||||
@@ -112,6 +157,30 @@
|
||||
{% if stage.status == "SUCCEEDED" %}Waited{% elif stage.status == "RUNNING" %}Waiting{% elif stage.status == "FAILED" %}Wait failed{% elif stage.status == "CANCELLED" %}Wait cancelled{% else %}Wait{% endif %}
|
||||
{% if stage.duration_seconds %}{{ stage.duration_seconds }}s{% endif %}
|
||||
</span>
|
||||
{% elif stage.stage_type == "plan" %}
|
||||
{% set plan_awaiting = stage.approval_status == "AWAITINGAPPROVAL" or stage.approval_status == "AWAITING_APPROVAL" or stage.status == "AWAITING_APPROVAL" %}
|
||||
<span class="text-sm {{ 'text-purple-700' if plan_awaiting else 'text-gray-700' if stage.status == 'SUCCEEDED' else 'text-yellow-700' if stage.status == 'RUNNING' else 'text-red-700' if stage.status == 'FAILED' else 'text-gray-400' }}">
|
||||
{% if stage.status == "SUCCEEDED" %}Plan approved{% elif plan_awaiting %}Awaiting plan approval{% elif stage.status == "RUNNING" %}Planning{% elif stage.status == "FAILED" %}Plan failed{% elif stage.status == "CANCELLED" %}Plan cancelled{% else %}Plan{% endif %}
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full {% if 'prod' in stage.environment and 'preprod' not in stage.environment %}bg-pink-100 text-pink-800{% elif 'preprod' in stage.environment or 'pre-prod' in stage.environment %}bg-orange-100 text-orange-800{% elif 'stag' in stage.environment %}bg-yellow-100 text-yellow-800{% elif 'dev' in stage.environment %}bg-violet-100 text-violet-800{% else %}bg-gray-100 text-gray-700{% endif %}">
|
||||
{{ stage.environment }}
|
||||
</span>
|
||||
{% if plan_awaiting and release_intent_id %}
|
||||
<div class="flex items-center gap-2 ml-auto">
|
||||
<form method="post" action="/api/orgs/{{ org_name }}/projects/{{ project_name }}/plan-stages/{{ stage.id }}/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="redirect_to" value="/orgs/{{ org_name }}/projects/{{ project_name }}/releases/{{ artifact.slug }}">
|
||||
<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 plan</button>
|
||||
</form>
|
||||
<form method="post" action="/api/orgs/{{ org_name }}/projects/{{ project_name }}/plan-stages/{{ stage.id }}/reject" class="inline" onsubmit="return confirm('Reject this plan?')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="release_intent_id" value="{{ release_intent_id }}">
|
||||
<input type="hidden" name="redirect_to" value="/orgs/{{ org_name }}/projects/{{ project_name }}/releases/{{ artifact.slug }}">
|
||||
<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 %}
|
||||
|
||||
{# Elapsed time #}
|
||||
@@ -146,17 +215,39 @@
|
||||
|
||||
{# ── Policy evaluations (approval, soak, branch) ──────────── #}
|
||||
{% if policy_evaluations | length > 0 %}
|
||||
{% set pns = namespace(passed=0, total=0, pending=0) %}
|
||||
{% for eval in policy_evaluations %}
|
||||
{% set pns.total = pns.total + 1 %}
|
||||
{% if eval.passed %}
|
||||
{% set pns.passed = pns.passed + 1 %}
|
||||
{% else %}
|
||||
{% set pns.pending = pns.pending + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<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">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<h2 class="text-sm font-semibold text-gray-900">Policy Requirements</h2>
|
||||
{% if pns.passed == pns.total %}
|
||||
<span class="inline-flex items-center gap-1 text-xs font-medium text-green-700 bg-green-50 px-2 py-0.5 rounded-full">
|
||||
<svg class="w-3 h-3" 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>
|
||||
{{ pns.passed }}/{{ pns.total }} passed
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center gap-1 text-xs font-medium text-amber-700 bg-amber-50 px-2 py-0.5 rounded-full">
|
||||
<svg class="w-3 h-3" 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>
|
||||
{{ pns.passed }}/{{ pns.total }} passed
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ── Pending / failed policies (expanded) ──────────────── #}
|
||||
{% if pns.pending > 0 %}
|
||||
<div class="border border-gray-200 rounded-lg divide-y divide-gray-100 mb-3">
|
||||
{% for eval in policy_evaluations %}
|
||||
{% if not eval.passed %}
|
||||
<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>
|
||||
@@ -196,42 +287,73 @@
|
||||
</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>
|
||||
{% 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_admin %}
|
||||
<form method="post" action="/orgs/{{ org_name }}/projects/{{ project_name }}/releases/{{ artifact.slug }}/approve" class="inline" onsubmit="return confirm('Admin bypass — skip remaining approvals?')">
|
||||
<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 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Passed policies (collapsed) ───────────────────────── #}
|
||||
{% if pns.passed > 0 %}
|
||||
<details class="border border-gray-200 rounded-lg group">
|
||||
<summary class="px-4 py-2.5 flex items-center gap-2 text-sm text-gray-500 cursor-pointer list-none hover:bg-gray-50">
|
||||
<svg class="w-3 h-3 text-gray-400 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
||||
<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>
|
||||
{{ pns.passed }} passed polic{{ "y" if pns.passed == 1 else "ies" }}
|
||||
</summary>
|
||||
<div class="divide-y divide-gray-100 border-t border-gray-100">
|
||||
{% for eval in policy_evaluations %}
|
||||
{% if eval.passed %}
|
||||
<div class="px-4 py-2.5 flex items-center gap-3 text-sm">
|
||||
<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>
|
||||
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user