Appearance
GitHub Actions Deploy Workflow
Each service repository that the IDP deploys needs a workflow_dispatch workflow the IDP can trigger. This page shows the exact deploy.yml used by the demo repositories (orders-service, payments-service, frontend-webapp in the idpnextdemo org).
The contract
The IDP dispatches the workflow with three inputs and then tracks the run:
| Input | Meaning |
|---|---|
environment | target environment (dev / staging / prod) — comes from the self-service form |
version | version to deploy — comes from the self-service form |
port_run_id | the IDP step callback id (sr_...). When set, the workflow reports its final status back to the IDP |
Status flows back to the IDP two ways — a push callback (fast) and externalId-correlation polling (fallback), so a failed callback never leaves a step stuck; it just syncs slower.
Full working example — .github/workflows/deploy.yml
yaml
name: deploy
on:
workflow_dispatch:
inputs:
environment:
description: Target environment
required: true
type: choice
options: [dev, staging, prod]
version:
description: Version to deploy
required: true
type: string
port_run_id:
description: IDP step callback id (sr_...) — when set, the workflow reports its status back to the IDP
required: false
type: string
default: ''
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
run: |
echo "Deploying $GITHUB_REPOSITORY version ${{ inputs.version }} to ${{ inputs.environment }}"
sleep 20
echo "Deployment of ${{ inputs.version }} to ${{ inputs.environment }} complete"
# Port-style callback: report the final status to the IDP so the step
# completes immediately. The IDP also polls this run's status (externalId
# correlation), so a failed/skipped callback is only a slower sync, never
# a stuck step.
- name: Report status to IDP
if: ${{ always() && inputs.port_run_id != '' }}
continue-on-error: true
env:
IDP_BASE_URL: ${{ vars.IDP_BASE_URL || 'https://idpnext.wazyneo.com' }}
IDP_CLIENT_ID: ${{ secrets.IDP_CLIENT_ID }}
IDP_CLIENT_SECRET: ${{ secrets.IDP_CLIENT_SECRET }}
PORT_RUN_ID: ${{ inputs.port_run_id }}
JOB_STATUS: ${{ job.status }}
run: |
if [ -z "$IDP_CLIENT_ID" ] || [ -z "$IDP_CLIENT_SECRET" ]; then
echo "IDP_CLIENT_ID/IDP_CLIENT_SECRET not configured; skipping IDP callback"
exit 0
fi
TOKEN=$(curl -sf -X POST "$IDP_BASE_URL/v1/auth/access_token" \
-H 'Content-Type: application/json' \
-d "{\"clientId\":\"$IDP_CLIENT_ID\",\"clientSecret\":\"$IDP_CLIENT_SECRET\"}" | jq -r .accessToken)
STATUS=$([ "$JOB_STATUS" = "success" ] && echo SUCCESS || echo FAILURE)
curl -sf -X PATCH "$IDP_BASE_URL/v1/actions/runs/$PORT_RUN_ID" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"status\":\"$STATUS\",\"statusLabel\":\"GitHub workflow $JOB_STATUS (reported by callback)\",\"externalRunId\":\"$GITHUB_RUN_ID\"}"
echo "Reported $STATUS for $PORT_RUN_ID to $IDP_BASE_URL"Replace the Deploy step's echo/sleep with your real deployment commands (helm upgrade, terraform apply, etc.) — everything else stays as-is.
Step by step
- Inputs — declared exactly as the IDP sends them.
port_run_idmust default to''so the workflow also works when triggered by a human. - Deploy job — your real deployment logic. Its
job.statusis what gets reported. - Report status to IDP:
if: always() && inputs.port_run_id != ''— runs on success and failure, but only for IDP-triggered runs.continue-on-error: true— a broken callback must not fail your deployment.- Authenticates with a service-account (
IDP_CLIENT_ID/IDP_CLIENT_SECRETrepo secrets — same account as the ingestion). PATCH /v1/actions/runs/{port_run_id}withSUCCESS/FAILUREand theexternalRunId(the GitHub run id, used for correlation).
Repository secrets
| Secret / Variable | Value |
|---|---|
IDP_CLIENT_ID, IDP_CLIENT_SECRET (secrets) | IDP service-account credentials |
IDP_BASE_URL (variable, optional) | your IDP URL — defaults to the demo URL in the example |
Copy deploy.yml into every repository your workflows will deploy.
