Skip to main content

Headless and agent onboarding

The supported headless flow sends a Morph API key, Morph organization id, and repository identity to Morph. It does not send a user's GitHub token to Morph. Keep any gh credential local.

Headless onboarding still contains a human checkpoint: a GitHub owner or repository administrator must approve the Morph GitHub App and select the exact private repository. An agent can stop at that checkpoint and resume from a new process by reading server state again.

The supported boundary is a private repository with trusted workflow authors.

For request and response schemas, use the curated Actions onboarding API reference. It is generated only from the public /v1/onboarding/* contract and intentionally excludes the service-wide operator schema.

Repository ownership is proved by two checks together: the caller is an admin of the named Morph organization, and the Morph GitHub App can access the exact repository through the installation bound to that tenant. A repository name or user GitHub credential alone is not ownership proof.

Shell setup

Use strict shell behavior and keep secrets out of command arguments, URLs, logs, and support payloads:

set -euo pipefail

export ACTIONS_API="https://actions.svc.cloud.morph.so/v1"
export MORPH_ORG_ID="org_..."
export REPOSITORY="owner/private-repository"
export MORPH_API_KEY="..."

actions_curl() {
curl --fail-with-body --silent --show-error "$@" \
-H "Authorization: Bearer $MORPH_API_KEY"
}

Do not enable shell tracing while secrets are present.

1. Observe before mutating

actions_curl --get "$ACTIONS_API/onboarding/status" \
--data-urlencode "morph_org_id=$MORPH_ORG_ID" \
--data-urlencode "repository=$REPOSITORY" \
| jq '{api_version, status, reason_code, repository, github_installation,
project, migration, dispatch_readiness, verification, next_actions}'

Use typed fields for control flow:

  • ready: continue to migration or verification;
  • an action with kind: open_url: pause for the named human actor;
  • kind: retry_request: retry its resume_endpoint when its condition is met;
  • status: failed with reason_code: public_repository_unsupported, retryable: false, or a conflict: stop and follow the bounded guidance.

Do not parse message or description to decide the next step.

2. Create or recover repository setup

jq -n \
--arg morph_org_id "$MORPH_ORG_ID" \
--arg repository "$REPOSITORY" \
'{morph_org_id: $morph_org_id, repository_full_name: $repository}' \
| actions_curl -X POST "$ACTIONS_API/onboarding/github/setup" \
-H "Content-Type: application/json" \
--data-binary @- \
| jq '{api_version, status, reason_code, retryable, request_id,
repository, github_installation, project, next_actions}'

If next_actions contains open_url, show that URL only to the authorized human. It points to a short-lived, single-use Morph handoff bound to the Morph organization and requested repository—not a bare GitHub App URL. Treat the complete URL as a bearer capability and never put it in logs or tickets.

After GitHub approval, discard the prior response and rerun status/setup. That works after an agent restart. Repeated setup observes existing App access and links before creating another handoff, so a successful installation does not need to be repeated.

3. Analyze and review migration

jq -n \
--arg morph_org_id "$MORPH_ORG_ID" \
--arg repository "$REPOSITORY" \
'{morph_org_id: $morph_org_id, repository: $repository,
preferences: {shape: "small", static_morph_labels: true}}' \
| actions_curl -X POST "$ACTIONS_API/onboarding/migration/analyze" \
-H "Content-Type: application/json" \
--data-binary @- \
| tee /tmp/morph-actions-analysis.json \
| jq '{proposed_runs_on, jobs_requiring_update, jobs_unsupported,
security_warnings, can_open_pull_request, manual_required,
existing_pull_request}'

Review workflow triggers, token permissions, secret use, third-party actions, containers/privileges, every changed label set, and every unsupported job. The analyzer performs supported runner-label rewriting, not a complete workflow safety assessment.

After review, submit the same canonical repository/source/preferences to POST /onboarding/migration/pull-request. Morph reuses only an exactly matching receipt and open pull request. A changed source, labels, base/head, or unrelated reserved branch produces a conflict instead of overwriting work. Review and merge the pull request in GitHub before dispatching its target ref.

4. Dispatch once and keep the receipt

Generate an idempotency key locally and persist the safe receipt id returned by the service:

export WORKFLOW_ID="morph-actions-smoke.yml"
export REF="main"
export DISPATCH_KEY="$(python3 -c 'import uuid; print(uuid.uuid4())')"

jq -n \
--arg morph_org_id "$MORPH_ORG_ID" \
--arg repository "$REPOSITORY" \
--arg workflow_id "$WORKFLOW_ID" \
--arg ref "$REF" \
--arg idempotency_key "$DISPATCH_KEY" \
'{morph_org_id: $morph_org_id, repository: $repository,
workflow_id: $workflow_id, ref: $ref, inputs: {},
idempotency_key: $idempotency_key}' \
| actions_curl -X POST "$ACTIONS_API/onboarding/dispatches" \
-H "Content-Type: application/json" \
--data-binary @- \
| tee /tmp/morph-actions-dispatch.json \
| jq '{status, receipt_id, idempotency_key, reused, lookup}'

If the client loses the response, look up the saved receipt with GET /onboarding/dispatches/receipts/{receipt_id}. Reuse an idempotency key only for the byte-equivalent canonical request. A changed or indeterminate request conflicts rather than risking a duplicate GitHub dispatch.

5. Poll exact verification

Once the receipt/run lookup yields a GitHub run id, poll verification with a bounded loop. Replace GITHUB_RUN_ID with the observed integer:

export GITHUB_RUN_ID="123456789"

for attempt in $(seq 1 30); do
response="$({
jq -n \
--arg morph_org_id "$MORPH_ORG_ID" \
--arg repository "$REPOSITORY" \
--argjson github_run_id "$GITHUB_RUN_ID" \
'{morph_org_id: $morph_org_id, repository: $repository,
github_run_id: $github_run_id}' \
| actions_curl -X POST "$ACTIONS_API/onboarding/verify" \
-H "Content-Type: application/json" --data-binary @-
} 2>/dev/null)"

state="$(jq -r '.status' <<<"$response")"
if [ "$state" = "verified" ]; then
jq -e '
.runner_v2_status == "succeeded" and
.runner_v2_assignment_state == "verified" and
(.github_job_id != null) and
(.runner_v2_actual_github_job_id == .github_job_id) and
.github_job_status == "completed" and
.github_job_conclusion == "success"
' <<<"$response" >/dev/null
break
fi
[ "$state" = "pending" ] || {
jq '{status, failure_reason, github_run_id, github_job_id}' <<<"$response"
exit 1
}
[ "$attempt" -lt 30 ] || exit 1
sleep 10
done

pending, mismatch, orphaned, and execution failure are not success. An unrelated verified run in the same Morph organization is irrelevant.

Recovery and support

If GitHub installation succeeds but the callback cannot correlate, preserve the installation and rerun status/setup. Follow the typed outcome guidance in Troubleshooting. Send support only the request id, Morph org id, repository, and approximate time—never an API key, GitHub token, cookie, state value, or handoff URL.