Skip to content

Answer a Gate ​

Send an event to an open Gate of a run from the CLI, the Console, a webhook handler or a CI job.

A run waits at a Gate until a person or a different system answers it. The Gate is open while the run is in the state that invokes it, and it closes when the run leaves that state. An open Gate has an id, its events, and the input schema of each event. Its meta is the data that the Machine shows to the person or the system that answers.

The examples on this page use the Workflow release. Its Gate review takes approve, with no input, and reject, with a reason. The Workflow has no Agent, so you can try the steps with no model. Save the file as workflows/release.ts in your Instance. Then run jr2 up:

ts
import { assign } from "xstate";
import { z } from "zod";
import { defineEvent, fail, jr2Setup } from "@jr2/orchestrator";

// The two answers that the Gate takes. A person, a script or a CI job sends one of them.
const approve = defineEvent({
  name: "approve",
  description: "Release this version.",
  input: z.object({}),
});
const reject = defineEvent({
  name: "reject",
  description: "Do not release this version. Say why.",
  input: z.object({ reason: z.string().min(1) }),
});

type Context = { version: string; reason?: string };

export const machine = jr2Setup({
  types: {} as { input: { version: string }; context: Context },
  events: [approve, reject],
  actors: { fail: fail() },
}).createMachine({
  id: "release",
  // jr2 checks each run input against this schema before the run starts.
  input: z.object({ version: z.string() }),
  context: ({ input }) => ({ version: input.version }),
  initial: "review",
  states: {
    review: {
      // The Gate is open while the run is in this state. Its id is the state key, "review".
      // It takes the events of this state: approve and reject.
      invoke: { src: "gate", input: ({ context }) => ({ meta: { version: context.version } }) },
      on: {
        approve: "released",
        reject: { target: "rejected", actions: assign({ reason: ({ event }) => event.reason }) },
      },
    },
    released: { type: "final" },
    // A person stopped the release, and the run ends here. Not a final state: the run ends
    // `failed`, with the person's reason.
    rejected: { invoke: { src: "fail", input: ({ context }) => ({ reason: context.reason! }) } },
  },
});

In the commands, replace these values:

  • <namespace>: the namespace of your Instance. It is the name in jr2.config.ts. When jr2.config.ts sets no name, it is the name of the Instance folder.
  • <run id>: the run id, or its first 4 characters or more. jr2 run prints the run id in the line run <run id> (<workflow>) — attached.

Give each jr2 command in the Instance folder.

Start a run that waits ​

Start a run in one terminal. jr2 run stays attached while the run waits at the Gate:

sh
jr2 run release --input '{"version":"1.4.0"}'
text
→ context <context> / namespace <namespace>
run 2319932e-02c1-4f9d-98db-3c5a410395da (release) — attached
  → active "review"

Your run id is different, and so are the instance id and the assembly hash in the next steps. Give the commands in the next steps in a second terminal. Start a run gives the other ways to start a run.

Find the open Gate ​

Run jr2 status with the run id:

sh
jr2 status 2319
text
→ context <context> / namespace <namespace>
{"runId":"2319932e-02c1-4f9d-98db-3c5a410395da","workflow":"release","instanceId":"c898d8c7-e48b-4a56-8101-7148905b6aed","status":"active","value":"review","context":{"version":"1.4.0"},"children":[],"waiting":[],"held":[],"assembly":"83858e28b054","gates":[{"gate":"review","path":["review"],"accepts":[{"name":"approve","description":"Release this version.","input":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{},"additionalProperties":false}},{"name":"reject","description":"Do not release this version. Say why.","input":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"reason":{"type":"string","minLength":1}},"required":["reason"],"additionalProperties":false}}],"acceptsNow":["approve","reject"],"meta":{"version":"1.4.0"}}]}

gates holds each open Gate of the run, as a GateView:

  • gate is the id of the Gate. Give it in --gate. When the Machine gives the Gate no id, jr2 makes the id from the path of the Gate's state. Here it is review, the key of the state.
  • accepts holds each event that the Gate accepts: its name, its description, and its input as JSON Schema.
  • acceptsNow holds the events that the run takes now. A guard on a transition can remove an event from this list.
  • meta holds the data that the Machine gives to the Gate.

A run that has no open Gate has "gates":[].

Answer from the CLI ​

Run jr2 send. Give the Gate in --gate, the event in --event, and the input of the event as a JSON object in --input:

sh
jr2 send 2319 --gate review --event reject --input '{"reason":"The changelog is not ready."}'
text
→ context <context> / namespace <namespace>
delivered reject to gate "review" on 2319932e-02c1-4f9d-98db-3c5a410395da

jr2 send prints delivered only when the run took the event. In the first terminal, the run ends. A reject ends the release, so the rejected state invokes fail with the reason, and the run ends failed. Its output is the Failure, and jr2 run exits with 1:

text
  → active "rejected"
  → failed "rejected"
failed: The changelog is not ready.
{"runId":"2319932e-02c1-4f9d-98db-3c5a410395da","workflow":"release","instanceId":"c898d8c7-e48b-4a56-8101-7148905b6aed","status":"failed","value":"rejected","context":{"version":"1.4.0","reason":"The changelog is not ready."},"children":[],"waiting":[],"held":[],"assembly":"83858e28b054","output":{"reason":"The changelog is not ready."}}

The run ended, so its Gate closed. Each example below answers a run that waits. Before each example, start a new run in the first terminal:

sh
jr2 run release --input '{"version":"1.4.0"}'

For an event with no input, such as approve, do not give --input:

sh
jr2 send <run id> --gate review --event approve
text
→ context <context> / namespace <namespace>
delivered approve to gate "review" on c099e2dc-6dd2-45da-9358-0723c8b83c68

jr2 send prints the full run id, also when you give a prefix.

Answer from the Console ​

You need the Instance token to answer a Gate from the Console. Open the Console tells how to open it and how to get the token.

  1. Open the Console.
  2. Type the Instance token in the instance token box in the nav.
  3. Push Enter. The badge beside the box shows …, then live.
  4. Click the Attention tab in the drawer on the right. It shows a card for each open Gate of the Workflow that you selected. To see the Gates of all the Workflows, select all workflows. When you select no Workflow, it shows them all.
  5. Find the card. Its top line shows ⚑ and the id of the Gate at the left. At the right, it shows the Workflow and the first 8 characters of the run id. Below it, the card shows the meta of the Gate.
  6. The card has a form for each event that the Gate accepts. Fill in the form of the event that you send. A field that has * after its name is necessary.
  7. Click send in that form.

When the run takes the event, the card goes away. When the Orchestrator refuses the event, the form shows the refusal text below the send button.

The ⚑ badge in the nav gives the number of open Gates in all the Workflows. Click it to open the Attention tab.

Answer from a webhook or CI ​

A webhook handler or a CI job answers a Gate with POST /runs/:runId/gates/:gate/events and the Instance token. jr2 has no webhook receiver of its own. Your handler or job reads its own event, finds the run and the Gate, and sends the call. The body is the event: type is the name of the event, and the other properties are its input.

jr2 up makes the Service jr2-orchestrator on port 4000, with no address outside the cluster. Thus a caller outside the cluster uses a port forward, or an address that you make for the Service.

In a new terminal, forward a port to the Orchestrator. Keep this command open:

sh
kubectl -n <namespace> port-forward svc/jr2-orchestrator 4000:4000

In a different terminal, get the Instance token into a variable:

sh
TOKEN=$(kubectl -n <namespace> get secret jr2-instance -o jsonpath='{.data.JR2_INSTANCE_TOKEN}' | base64 -d)

Send the event. Use the full run id, because the route does not accept a prefix:

sh
curl -s -X POST http://localhost:4000/runs/<run id>/gates/review/events \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type":"approve"}'
text
{"ok":true}

A webhook handler often knows a fact, such as a version, but not the run id. It can find the run from the meta of the Gate. GET /runs gives the live runs, and GET /runs/:runId gives the open Gates of one run. This script approves the release run that waits for one version. It needs curl and jq. Save it as approve-release.sh:

sh
#!/bin/sh
# Approve the release run that waits at its review Gate for one version.
# Set JR2_URL, JR2_TOKEN and VERSION before you run this script. It needs curl and jq.
set -eu

auth="Authorization: Bearer $JR2_TOKEN"

# The live runs of the release Workflow.
for run in $(curl -fsS -H "$auth" "$JR2_URL/runs" | jq -r '.[] | select(.workflow == "release") | .runId'); do
  # The open Gates of the run, with their meta.
  version=$(curl -fsS -H "$auth" "$JR2_URL/runs/$run" | jq -r '.gates[] | select(.gate == "review") | .meta.version')
  if [ "$version" = "$VERSION" ]; then
    curl -fsS -X POST -H "$auth" -H "Content-Type: application/json" \
      -d '{"type":"approve"}' "$JR2_URL/runs/$run/gates/review/events"
    echo
  fi
done
sh
JR2_URL=http://localhost:4000 JR2_TOKEN=$TOKEN VERSION=1.4.0 sh approve-release.sh
text
{"ok":true}

A CI job can also run jr2 send. With JR2_URL and JR2_TOKEN, jr2 send needs no Instance folder and no kubectl context, and it accepts a prefix of the run id:

sh
JR2_URL=http://localhost:4000 JR2_TOKEN=$TOKEN jr2 send <run id> --gate review --event approve
text
→ http://localhost:4000
delivered approve to gate "review" on c44c6e49-452d-4150-8ce4-4d036363d2a0

Rules ​

  • The Orchestrator refuses a Gate that is not open on the run, with the status code 404: no open gate "<gate>" on run "<run id>" (open: <gates>). The part (open: <gates>) shows only when the run has an open Gate. When an event takes the run out of the state of the Gate, the Gate closes. Thus a second answer gets this refusal.
  • The Orchestrator refuses an event that the Gate does not accept, with the status code 400: "<gate>" does not accept "<event>" (accepts: <events>)
  • The Orchestrator refuses an input that does not agree with the schema of the event, with the status code 400: invalid "<event>" payload: <issues>. <issues> is the list of the problems as JSON.
  • The Orchestrator refuses an event that the run does not take now, with the status code 409, and delivers nothing: gate "<gate>" is open, and the run does not take "<event>" now: <why> — not delivered (takes now: <events>). The body also holds acceptsNow.
  • The Orchestrator refuses a body with no type, with the status code 400: event body must carry a string "type" (one of the gate's accepted names)
  • The Orchestrator refuses a body that holds sender, with the status code 400: `sender` is written by the Orchestrator on every delivered event; a caller cannot send it
  • The Orchestrator refuses an event without the Instance token, with the status code 401: unauthorized.
  • The Orchestrator refuses an event with the token of a Sandbox, with the status code 403: a Sandbox token cannot deliver to a gate — gates are not on the Agent's surface. Thus an Agent cannot answer a Gate.
  • jr2 send prints each refusal of the Orchestrator as error: <refusal>, and exits with 1. After a 404 refusal, it also prints the version of the Instance in one more line: instance: <version> (<hash>) — if it predates this CLI, `jr2 up` converges the cluster to this kit. <hash> is the hash of the current Assembly.
  • jr2 send without --gate sends run control, and run control accepts only CANCEL. For a different event, it exits with 2: jr2 send: unknown event "<event>" (run control accepts: CANCEL; workflow events need --gate)
  • With --gate, jr2 send sends nothing when --input is not valid JSON. It exits with 2: jr2 send: --input is not valid JSON
  • jr2 send with no run id or no --event sends nothing. It prints its help and exits with 2.

The docs of Kit version 0.5.1. Each page is also at its .md URL.