Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/deploy-launchplane.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ jobs:
LAUNCHPLANE_LOCAL_OPERATOR_TOKEN: ${{ secrets.LAUNCHPLANE_LOCAL_OPERATOR_TOKEN }}
LAUNCHPLANE_LOCAL_OPERATOR_SUBJECT: ${{ vars.LAUNCHPLANE_LOCAL_OPERATOR_SUBJECT }}
LAUNCHPLANE_LOCAL_OPERATOR_TOKEN_LABEL: ${{ vars.LAUNCHPLANE_LOCAL_OPERATOR_TOKEN_LABEL }}
LAUNCHPLANE_PRODUCT_CONFIG_OPERATOR_LOGINS: ${{ vars.LAUNCHPLANE_PRODUCT_CONFIG_OPERATOR_LOGINS }}
LAUNCHPLANE_PRODUCT_CONFIG_OPERATOR_PRODUCTS: ${{ vars.LAUNCHPLANE_PRODUCT_CONFIG_OPERATOR_PRODUCTS }}
LAUNCHPLANE_PRODUCT_CONFIG_OPERATOR_CONTEXTS: ${{ vars.LAUNCHPLANE_PRODUCT_CONFIG_OPERATOR_CONTEXTS }}
ODOO_CM_TESTING_DOKPLOY_TARGET_ID: ${{ vars.ODOO_CM_TESTING_DOKPLOY_TARGET_ID }}
ODOO_CM_PROD_DOKPLOY_TARGET_ID: ${{ vars.ODOO_CM_PROD_DOKPLOY_TARGET_ID }}
ODOO_OPW_TESTING_DOKPLOY_TARGET_ID: ${{ vars.ODOO_OPW_TESTING_DOKPLOY_TARGET_ID }}
Expand Down
112 changes: 75 additions & 37 deletions control_plane/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2666,7 +2666,7 @@ def _latest_merge_train_batch_candidate_record(
latest_record = _latest_merge_train_batch_candidate_progress_record(records)
if latest_record is None:
return None
terminal_statuses = {"passed", "failed", "stale", "blocked"}
terminal_statuses = {"passed", "stale", "blocked"}
if latest_record.candidate.status in terminal_statuses:
return None
return latest_record
Expand Down Expand Up @@ -2757,6 +2757,18 @@ def _validate_merge_train_landing_record_for_controller(
raise ValueError("merge train landing plan policy digest no longer matches")


def _validate_merge_train_stack_collapse_record_for_controller(
*,
collapse_record: MergeTrainStackCollapsePlanRecord,
policy_key: str,
policy_sha256: str,
) -> None:
if collapse_record.plan.policy_key != policy_key:
raise ValueError("merge train stack collapse policy key no longer matches")
if collapse_record.plan.policy_sha256 != policy_sha256:
raise ValueError("merge train stack collapse policy digest no longer matches")


def _latest_merge_train_batch_candidate_progress_record(
records: tuple[MergeTrainBatchCandidateRecord, ...],
) -> MergeTrainBatchCandidateRecord | None:
Expand Down Expand Up @@ -7941,45 +7953,61 @@ def product_action_allowed(
policy_key=repository_policy.policy_key,
policy_sha256=policy_record.policy_sha256,
)
if active_candidate_record.candidate.status in {"planned", "building"}:
controller_action = "build_candidate"
if controller_request.mutate:
candidate = github_client.build_batch_candidate(
candidate=active_candidate_record.candidate
)
else:
candidate = active_candidate_record.candidate
if active_candidate_record.candidate.status == "failed":
result = {
"repository": controller_request.repository,
"base_branch": controller_request.base_branch,
"mode": "dry-run",
"controller_action": "candidate_failed",
"merge_train_batch_candidate_record_id": active_candidate_record.record_id,
"candidate": active_candidate_record.candidate.model_dump(
mode="json"
),
}
driver_result = result
else:
controller_action = "observe_candidate"
if active_candidate_record.candidate.status in {
"planned",
"building",
}:
controller_action = "build_candidate"
if controller_request.mutate:
candidate = github_client.build_batch_candidate(
candidate=active_candidate_record.candidate
)
else:
candidate = active_candidate_record.candidate
else:
controller_action = "observe_candidate"
if controller_request.mutate:
candidate = github_client.observe_batch_candidate_checks(
candidate=active_candidate_record.candidate
)
else:
candidate = active_candidate_record.candidate
result = {
"repository": controller_request.repository,
"base_branch": controller_request.base_branch,
"mode": "dry-run"
if not controller_request.mutate
else controller_action,
"controller_action": controller_action,
"merge_train_batch_candidate_record_id": active_candidate_record.record_id,
}
if controller_request.mutate:
candidate = github_client.observe_batch_candidate_checks(
candidate=active_candidate_record.candidate
updated_candidate_record = build_merge_train_batch_candidate_record(
candidate=candidate,
source=f"service:controller:{controller_action}:{request_trace_id}",
updated_at=recorded_at,
)
else:
candidate = active_candidate_record.candidate
result = {
"repository": controller_request.repository,
"base_branch": controller_request.base_branch,
"mode": "dry-run"
if not controller_request.mutate
else controller_action,
"controller_action": controller_action,
"merge_train_batch_candidate_record_id": active_candidate_record.record_id,
}
if controller_request.mutate:
updated_candidate_record = build_merge_train_batch_candidate_record(
candidate=candidate,
source=f"service:controller:{controller_action}:{request_trace_id}",
updated_at=recorded_at,
)
candidate_store.write_merge_train_batch_candidate_record(
updated_candidate_record
)
result["merge_train_batch_candidate_record_id"] = (
updated_candidate_record.record_id
)
result["candidate"] = candidate.model_dump(mode="json")
driver_result = result
candidate_store.write_merge_train_batch_candidate_record(
updated_candidate_record
)
result["merge_train_batch_candidate_record_id"] = (
updated_candidate_record.record_id
)
result["candidate"] = candidate.model_dump(mode="json")
driver_result = result
elif passed_candidate_record is not None:
_validate_merge_train_candidate_record_for_controller(
candidate_record=passed_candidate_record,
Expand Down Expand Up @@ -8026,6 +8054,11 @@ def product_action_allowed(
plan_status="waiting_for_root_checks",
)
if waiting_collapse_record is not None:
_validate_merge_train_stack_collapse_record_for_controller(
collapse_record=waiting_collapse_record,
policy_key=repository_policy.policy_key,
policy_sha256=policy_record.policy_sha256,
)
snapshot = GitHubMergeTrainSnapshotReader(
transport=transport
).read_merge_train_snapshot(
Expand Down Expand Up @@ -8111,6 +8144,11 @@ def product_action_allowed(
)
)
if planned_collapse_record is not None:
_validate_merge_train_stack_collapse_record_for_controller(
collapse_record=planned_collapse_record,
policy_key=repository_policy.policy_key,
policy_sha256=policy_record.policy_sha256,
)
result = {
"repository": controller_request.repository,
"base_branch": controller_request.base_branch,
Expand Down
11 changes: 11 additions & 0 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ short-lived bearer token or a Launchplane browser session cookie, and the
service validates the caller, writes any new active policy record, stores audit
metadata, and reloads the current service worker's active policy.

The Launchplane deploy workflow also reconciles configured signed-in operator
grants for product-config writes. Set
`LAUNCHPLANE_PRODUCT_CONFIG_OPERATOR_LOGINS`,
`LAUNCHPLANE_PRODUCT_CONFIG_OPERATOR_PRODUCTS`, and
`LAUNCHPLANE_PRODUCT_CONFIG_OPERATOR_CONTEXTS` as comma-separated repository
variables. During deploy, `scripts/deploy/ensure-authz-grants.sh` writes
DB-backed GitHub-human grants for `product_config.plan` and
`product_config.apply` through `/v1/authz-policies/github-humans/grants`.
Leave those variables unset to skip reconciliation; do not hard-code human
logins or product-specific operator grants in source.

The deploy workflow maintains DB-backed grants for SellYourOutboard operational
workflows, including product profile cutover reads/writes, production promotion,
and generic-web preview refresh/destroy requests. The grant request returns only
Expand Down
69 changes: 69 additions & 0 deletions scripts/deploy/ensure-authz-grants.sh
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,67 @@ post_terminal_agent_grant() {
return 1
}

post_product_config_human_grant() {
local action_name="$1"
local source_label="$2"
local idempotency_suffix="$3"
local operator_logins="${LAUNCHPLANE_PRODUCT_CONFIG_OPERATOR_LOGINS:-}"
local operator_products="${LAUNCHPLANE_PRODUCT_CONFIG_OPERATOR_PRODUCTS:-}"
local operator_contexts="${LAUNCHPLANE_PRODUCT_CONFIG_OPERATOR_CONTEXTS:-}"
local request_payload response_file status_code

if [ -z "$operator_logins" ] || [ -z "$operator_products" ] || [ -z "$operator_contexts" ]; then
echo "Skipping product-config human grant ${source_label}; operator login/product/context variables are not fully configured."
return 0
fi

request_payload="$({
jq -n \
--arg logins "$operator_logins" \
--arg products "$operator_products" \
--arg contexts "$operator_contexts" \
--arg action_name "$action_name" \
--arg source_label "$source_label" \
'def csv_list($value):
$value
| split(",")
| map(gsub("^\\s+|\\s+$"; ""))
| map(select(length > 0));
{
schema_version: 1,
product: "launchplane",
mode: "apply",
reason: ("Deploy workflow ensuring product-config human authz grant " + $source_label),
related_issue: "cbusillo/launchplane#521",
grant: {
logins: csv_list($logins),
roles: ["admin"],
products: csv_list($products),
contexts: csv_list($contexts),
actions: [$action_name],
source_label: $source_label
}
}'
})"
response_file="$(mktemp)"
status_code="$(curl -sS \
-o "$response_file" \
-w '%{http_code}' \
-X POST \
-H "Authorization: Bearer ${oidc_token}" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: launchplane-product-config-human-grant:${idempotency_suffix}:${GITHUB_SHA}" \
--data "$request_payload" \
"${LAUNCHPLANE_SERVICE_URL}/v1/authz-policies/github-humans/grants")"
if [ "$status_code" = "200" ] || [ "$status_code" = "202" ]; then
cat "$response_file"
return 0
fi
cat "$response_file" >&2
echo "Launchplane product-config human authz grant request failed with HTTP ${status_code}." >&2
return 1
}

apply_product_onboarding() {
local idempotency_suffix="$1"
local request_payload response_file status_code
Expand Down Expand Up @@ -772,6 +833,14 @@ post_terminal_agent_grant \
every_code_preview_gate.read \
deploy:terminal-agent-agent-context-preview-grant \
terminal-agent-agent-context-preview
post_product_config_human_grant \
product_config.plan \
deploy:product-config-human-plan-grant \
product-config-human-plan
post_product_config_human_grant \
product_config.apply \
deploy:product-config-human-apply-grant \
product-config-human-apply
post_grant \
"$GITHUB_REPOSITORY" \
live-target-runtime.yml \
Expand Down
Loading