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

@@ -7,7 +7,7 @@
<h1 class="text-2xl font-bold">Deployment Policies</h1>
<p class="text-sm text-gray-500 mt-1">
<a href="/orgs/{{ current_org }}/projects/{{ current_project }}" class="hover:underline">{{ current_project }}</a>
&middot; Gate deployments with soak times and branch restrictions
&middot; Gate deployments with soak times, branch restrictions, and approvals
</p>
</div>
</div>
@@ -39,6 +39,11 @@
<code class="bg-gray-100 px-1.5 py-0.5 rounded">{{ policy.config.branch_pattern }}</code>
<span class="text-gray-300">&#8594;</span>
<code class="bg-gray-100 px-1.5 py-0.5 rounded">{{ policy.config.target_environment }}</code>
{% elif policy.policy_type == "approval" %}
<span class="bg-emerald-100 text-emerald-700 px-1.5 py-0.5 rounded">Approval Required</span>
<code class="bg-gray-100 px-1.5 py-0.5 rounded">{{ policy.config.target_environment }}</code>
<span class="text-gray-400">&middot;</span>
<span>{{ policy.config.required_approvals }} approval{{ 's' if policy.config.required_approvals != 1 }}</span>
{% endif %}
</div>
@@ -68,7 +73,7 @@
<div class="border border-dashed border-gray-300 rounded-lg p-8 text-center text-gray-500 mb-8">
<p class="mb-1">No deployment policies configured.</p>
{% if is_admin %}
<p class="text-sm">Create one below to gate deployments with soak times or branch restrictions.</p>
<p class="text-sm">Create one below to gate deployments with soak times, branch restrictions, or approvals.</p>
{% endif %}
</div>
{% endif %}
@@ -91,6 +96,7 @@
class="border border-gray-300 rounded-md px-3 py-1.5 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-gray-900">
<option value="soak_time">Soak Time</option>
<option value="branch_restriction">Branch Restriction</option>
<option value="approval">Approval Required</option>
</select>
<p class="text-xs text-gray-500 mt-1" id="policy-type-desc">
Require an artifact to succeed in a source environment for a duration before deploying to target.
@@ -144,6 +150,26 @@
</div>
</div>
{# Approval fields #}
<div id="approval-fields" class="hidden">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Target environment</label>
<select name="target_environment" id="approval-target-env" class="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm bg-white">
{% for env in environments %}
<option value="{{ env.name }}">{{ env.name }}</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Required approvals</label>
<input type="number" name="required_approvals" min="1" value="1" placeholder="1"
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">
<p class="text-xs text-gray-500 mt-1">Number of distinct approvals needed before deployment proceeds.</p>
</div>
</div>
</div>
<button type="submit" class="bg-gray-900 text-white px-4 py-2 rounded-md text-sm hover:bg-gray-800 transition-colors">
Create Policy
</button>
@@ -153,21 +179,33 @@
<script>
(function() {
const typeSelect = document.getElementById('policy-type');
const soakFields = document.getElementById('soak-time-fields');
const branchFields = document.getElementById('branch-restriction-fields');
const sections = {
soak_time: document.getElementById('soak-time-fields'),
branch_restriction: document.getElementById('branch-restriction-fields'),
approval: document.getElementById('approval-fields'),
};
const desc = document.getElementById('policy-type-desc');
const descriptions = {
soak_time: 'Require an artifact to succeed in a source environment for a duration before deploying to target.',
branch_restriction: 'Only allow deployments to the target environment from a specific branch pattern.',
approval: 'Require one or more team members to approve before deploying to the target environment.',
};
typeSelect.addEventListener('change', () => {
const isSoak = typeSelect.value === 'soak_time';
soakFields.classList.toggle('hidden', !isSoak);
branchFields.classList.toggle('hidden', isSoak);
desc.textContent = descriptions[typeSelect.value] || '';
});
function toggle() {
const v = typeSelect.value;
for (const [key, el] of Object.entries(sections)) {
const active = key === v;
el.classList.toggle('hidden', !active);
el.querySelectorAll('input, select').forEach(function(inp) {
inp.disabled = !active;
});
}
desc.textContent = descriptions[v] || '';
}
typeSelect.addEventListener('change', toggle);
toggle();
})();
</script>
{% endif %}