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
76 changes: 73 additions & 3 deletions .github/workflows/reusable-generic-web-stable-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ permissions:

jobs:
stable-deploy:
if: ${{ inputs.recovery_request_json == '' }}
if: >-
inputs.recovery_request_json == '' &&
(github.event_name != 'workflow_dispatch' || github.event.inputs.original_run_id == '')
outputs:
deployment_record_id: ${{ steps.lp.outputs.deployment_record_id }}
deploy_status: ${{ steps.lp.outputs.deploy_status }}
Expand Down Expand Up @@ -161,7 +163,9 @@ jobs:
echo "post_deploy_status=$POST_DEPLOY_STATUS"

recovery-dry-run:
if: ${{ inputs.recovery_request_json != '' }}
if: >-
inputs.recovery_request_json != '' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.original_run_id != '')
outputs:
recovery_digest: ${{ steps.recovery.outputs.recovery_digest }}
proposed_action: ${{ steps.recovery.outputs.proposed_action }}
Expand All @@ -172,11 +176,77 @@ jobs:
observed_at: ${{ steps.recovery.outputs.observed_at }}
runs-on: ubuntu-latest
steps:
- name: Resolve Launchplane recovery request
id: request
env:
ARTIFACT_ID: ${{ inputs.artifact_id }}
EXPLICIT_REQUEST: ${{ inputs.recovery_request_json }}
INSTANCE: ${{ inputs.instance }}
ORIGINAL_RUN_ATTEMPT: ${{ github.event.inputs.original_run_attempt }}
ORIGINAL_RUN_ID: ${{ github.event.inputs.original_run_id }}
PRODUCT: ${{ inputs.product }}
REASON: ${{ github.event.inputs.reason }}
SOURCE_GIT_REF: ${{ inputs.source_git_ref }}
run: |
set -euo pipefail

if [ -n "$EXPLICIT_REQUEST" ]; then
request="$EXPLICIT_REQUEST"
else
if [ -z "$PRODUCT" ]; then
PRODUCT="${GITHUB_REPOSITORY#*/}"
fi
if [ -z "$INSTANCE" ]; then
INSTANCE="testing"
fi
for required in \
PRODUCT \
INSTANCE \
ARTIFACT_ID \
SOURCE_GIT_REF \
ORIGINAL_RUN_ID \
ORIGINAL_RUN_ATTEMPT \
REASON; do
if [ -z "${!required}" ]; then
echo "${required} is required for recovery dry-run." >&2
exit 1
fi
done

launchplane_url_json='${{ toJSON(inputs.launchplane_url || vars.LAUNCHPLANE_PUBLIC_URL) }}'
request="$(jq -cn \
--argjson launchplane_url "$launchplane_url_json" \
--arg product "$PRODUCT" \
--arg instance "$INSTANCE" \
--arg artifact_id "$ARTIFACT_ID" \
--arg source_git_ref "$SOURCE_GIT_REF" \
--arg original_run_id "$ORIGINAL_RUN_ID" \
--arg original_run_attempt "$ORIGINAL_RUN_ATTEMPT" \
--arg reason "$REASON" \
'{
schema_version: 1,
launchplane_url: $launchplane_url,
product: $product,
instance: $instance,
artifact_id: $artifact_id,
source_git_ref: $source_git_ref,
original_run_id: $original_run_id,
original_run_attempt: $original_run_attempt,
reason: $reason
}')"
fi

{
echo "request<<EOF"
echo "$request"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Request Launchplane recovery dry run
id: recovery
uses: cbusillo/launchplane/.github/actions/generic-web-deploy-recovery-dry-run@b2055d2944626234664390d6fcd96975ded38511 # main
with:
request-json: ${{ inputs.recovery_request_json }}
request-json: ${{ steps.request.outputs.request }}
timeout-ms: ${{ inputs['timeout-ms'] }}

- name: Report bounded recovery evidence
Expand Down
6 changes: 6 additions & 0 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,12 @@ workflow may pass that request object through the optional
`.github/workflows/reusable-generic-web-stable-deploy.yml`. A non-empty recovery
request skips the stable-deploy job and runs only the bounded dry-run action;
the reusable workflow exposes no recovery apply input or route.
For existing product connectors that must preserve an established reusable-job
identity, the workflow also recognizes a caller `workflow_dispatch` event with
non-empty `original_run_id`, `original_run_attempt`, and `reason` inputs. It
constructs the same private recovery envelope from the caller event plus the
existing product, instance, artifact, source, and Launchplane URL inputs, then
skips stable deploy exactly as the explicit envelope path does.

Stage 2 apply is explicit and digest-gated. Operators call
`POST /v1/admin/generic-web/deploy-recovery/apply` with the same request body as
Expand Down
10 changes: 7 additions & 3 deletions tests/test_generic_web_deploy_recovery_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,16 @@ def test_stable_deploy_reusable_workflow_has_dry_run_only_recovery_mode(self) ->

required_fragments = (
"recovery_request_json:",
"if: ${{ inputs.recovery_request_json == '' }}",
"if: ${{ inputs.recovery_request_json != '' }}",
"github.event.inputs.original_run_id == ''",
"github.event.inputs.original_run_id != ''",
"name: Resolve Launchplane recovery request",
"ORIGINAL_RUN_ATTEMPT: ${{ github.event.inputs.original_run_attempt }}",
"ORIGINAL_RUN_ID: ${{ github.event.inputs.original_run_id }}",
"REASON: ${{ github.event.inputs.reason }}",
"name: Request Launchplane recovery dry run",
"uses: cbusillo/launchplane/.github/actions/"
"generic-web-deploy-recovery-dry-run@b2055d2944626234664390d6fcd96975ded38511",
"request-json: ${{ inputs.recovery_request_json }}",
"request-json: ${{ steps.request.outputs.request }}",
"Recovery digest:",
"Proposed action:",
"Reservation state:",
Expand Down
Loading