Skip to content

feat(kotlin): Nexus decouple-a-monolith workshop and Instruqt track - #9

Merged
nadvolod merged 18 commits into
mainfrom
kotlin-nexus-workshop
Aug 12, 2026
Merged

feat(kotlin): Nexus decouple-a-monolith workshop and Instruqt track#9
nadvolod merged 18 commits into
mainfrom
kotlin-nexus-workshop

Conversation

@nadvolod

@nadvolod nadvolod commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Adds a Kotlin hands-on lab for Temporal Nexus, built for a live 90 minute
workshop with a 40 minute lab block. The Java tree under java/ is untouched.

What's here

  • kotlin/decouple-monolith/exercise — Gradle KTS project, TODOs 1 to 11 open
  • kotlin/decouple-monolith/solution — the same project, complete
  • kotlin/sandbox/Dockerfile — Temurin 21, Temporal CLI, code-server, warm Gradle cache
  • kotlin/diagrams/ — interactive monolith architecture diagram
  • kotlin/instruqt/ — track definition and five challenges
  • INSTRUQT.md — what this track cost us, written symptom-first
  • .github/workflows/ — reusable track CI plus a Kotlin caller scoped to kotlin/**

The lab

Payments and Compliance start in one Worker, on one Task Queue, in one Namespace.
Learners put a Nexus boundary between them without changing business logic:
define the shared Service contract, implement the handler on the Compliance side,
register the Endpoint, then swap the caller from an Activity stub to a Nexus stub.
One line changes at the call site.

Then they break it on purpose: stop the Compliance Worker mid-operation and watch
the payment Workflow wait instead of fail.

# Slug Format Files touched
01 run-the-monolith observe none
02 the-shared-contract code 1
03 the-compliance-side code 2 + the Endpoint CLI
04 the-payments-side code 2
05 durability-and-human-review observe none

Challenges are grouped by actor rather than one per TODO: the shared contract,
then everything the Compliance team owns, then everything the Payments team owns.
The grouping is itself the lesson.

Seven tabs

Exercise (code-server), Temporal UI, Terminal, Payments Worker, Compliance Worker,
Solution (code-server), Monolith Architecture.

The editors are code-server rather than Instruqt's native type: code tab, so
learners get Kotlin syntax highlighting. Exercise is deliberately the FIRST tab:
Instruqt loads every service tab's iframe at challenge start, and code-server
cannot lay itself out in a hidden 0x0 iframe. That was the fix for the editor
rendering blank until a manual refresh.

The architecture tab shows challenge 01's monolith with a step player. Payments
code in blue, Compliance code in amber, both inside one dashed process outline,
next to an empty compliance-namespace that fills up in challenge 4.

Deliberate divergences from java/

  • PaymentStarter starts the three Workflows without blocking. The Java starter
    is a sequential blocking loop, which deadlocks once TXN-B parks for human
    review: TXN-C would never start.
  • Workers print their banner with println. The Java exercise calls
    Workflow.getLogger from main, which throws off a Workflow thread.
  • Failures fail the Workflow. The broad catch (e: Exception) was swallowing
    CanceledFailure, so a cancelled Workflow reported success.
  • executePayment is idempotent. It derived the confirmation number from the
    clock, so each retry minted a different one, in a payments demo.

Reviewer notes

Merging publishes the track. build-and-push-kotlin.yml runs
instruqt track push --force on push to main touching kotlin/instruqt/**.
It needs a repo secret named INSTRUQT_TOKEN. The track is maintenance: true,
so it stays owner-only either way.

CI does not build the image, on purpose. The image lives in a personal GHCR
namespace, which this repo's GITHUB_TOKEN cannot write to, so a build job would
fail on every merge. It is built by hand and pinned by digest; the recipe is in
the workflow header and the README. Moving it to ghcr.io/temporalio/... later
is a one-line change back.

The image is pinned by digest. Any change under kotlin/decouple-monolith/
or kotlin/diagrams/ needs a rebuild AND a re-pin; only kotlin/instruqt/
changes are push-only. Pushing the track alone silently ships the old image.
This bit us: attendees read TODO text from six commits earlier.

Four of five check scripts are exit 0. Deliberate: this is instructor-led,
and Skip runs the solve script, which overwrites the learner's work. Gating
progression pushes a stuck learner toward destroying their own code. Challenge 04
keeps a check because its failure is silent: a mistyped Nexus Endpoint does not
fail the Workflow, it hangs in a Workflow-task retry loop.

validate-track.sh stops checking tab references after the first push. Its
counter only matches entries starting with - title:, but pushed tabs start with
- id:. Worth a one-line fix in temporal-edu-skills. Buttons here were verified
by label instead: 22 references, 0 mismatches.

Verified

Both trees compile. Solution runs end to end with the full Nexus event chain.
Durability tested five times with the Worker actually down. Exercise gate
confirmed: the Compliance Worker refuses to start until the contract and handler
are annotated. Five-challenge solve chain walked in a container from the pinned
image.

nadvolod and others added 5 commits August 11, 2026 18:58
build/, .gradle/, and the VS Code Kotlin language server's bin/ output.
gradle/wrapper/gradle-wrapper.jar is deliberately left tracked: the sandbox
image and learners both build without a local Gradle install.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Gradle KTS project on the Temporal Java SDK. The exercise tree ships five
TODOs: the Nexus Service contract, the Compliance handler and its Worker
registrations, and the caller-side stub swap. The solution tree is complete.

Payments and Compliance start in one Worker on one Task Queue. Learners put
a Nexus boundary between them without changing business logic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five challenges: observe the coupling, write the shared contract, implement
the Compliance handler and Endpoint, swap the caller to a Nexus stub, then
stop the handler Worker mid-payment and watch the payment wait instead of fail.

The sandbox bakes Temurin 21, the Temporal CLI, both Kotlin trees, and a warm
Gradle cache. Both trees compile at image build time, so a broken port fails
the build rather than the workshop.

The Nexus Endpoint is deliberately not created during setup. Attendees create
it themselves in challenge 3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reusable track-ci workflow plus a Kotlin caller scoped to kotlin/**, so the
Java tutorial tree is untouched. The sandbox image rebuilds only when baked
assets change; a prose-only edit under kotlin/instruqt/ pushes the track
without a rebuild.

Requires a repo secret named INSTRUQT_TOKEN and a public GHCR package.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Layout of kotlin/, what the sandbox image bakes in, the five challenges, and
why four of the five check scripts are exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 11, 2026 22:59
packages: write
steps:
- name: Checkout repo
uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.gha.unpinned-action (WARNING)

Unpinned action reference actions/checkout@v4: this uses: resolves a mutable ref (tag or branch), so the code that runs in CI can change without this line changing. A compromised upstream can repoint the tag and execute arbitrary code with access to this repository's secrets and GITHUB_TOKEN (tj-actions/changed-files, March 2025). Pin to the full 40-character commit SHA with the resolved version in a trailing comment, e.g. uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2. Prefer deputy pin --ecosystems github-actions, which resolves the ref, writes a version comment that reflects the most specific ref actually pointing at that commit, and verifies the SHA is reachable from a real branch upstream. That last check matters: pinning alone does not detect imposter or dangling commits, and this rule only sees the shape of the ref, never its provenance. Reusable workflow calls (owner/repo/.github/workflows/x.yml@ref) run with the same trust as actions and are pinned the same way. Not reported, by campaign policy: temporalio/* refs (first-party, pinned by internal process), local ./ actions, self-repository $/ refs (resolve to the running commit, so they are already pin-equivalent), and docker:// images (pinned by digest as a separate ecosystem).


Suppress this finding

Add a suppression comment on the line before:

# noopengrep: security.gha.unpinned-action

- name: Checkout repo
uses: actions/checkout@v4

- uses: docker/login-action@v3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.gha.unpinned-action (WARNING)

Unpinned action reference docker/login-action@v3: this uses: resolves a mutable ref (tag or branch), so the code that runs in CI can change without this line changing. A compromised upstream can repoint the tag and execute arbitrary code with access to this repository's secrets and GITHUB_TOKEN (tj-actions/changed-files, March 2025). Pin to the full 40-character commit SHA with the resolved version in a trailing comment, e.g. uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2. Prefer deputy pin --ecosystems github-actions, which resolves the ref, writes a version comment that reflects the most specific ref actually pointing at that commit, and verifies the SHA is reachable from a real branch upstream. That last check matters: pinning alone does not detect imposter or dangling commits, and this rule only sees the shape of the ref, never its provenance. Reusable workflow calls (owner/repo/.github/workflows/x.yml@ref) run with the same trust as actions and are pinned the same way. Not reported, by campaign policy: temporalio/* refs (first-party, pinned by internal process), local ./ actions, self-repository $/ refs (resolve to the running commit, so they are already pin-equivalent), and docker:// images (pinned by digest as a separate ecosystem).


Suppress this finding

Add a suppression comment on the line before:

# noopengrep: security.gha.unpinned-action

echo "extra=${IMAGE}:${REF_NAME}" >> "$GITHUB_OUTPUT"
fi

- uses: docker/build-push-action@v6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.gha.unpinned-action (WARNING)

Unpinned action reference docker/build-push-action@v6: this uses: resolves a mutable ref (tag or branch), so the code that runs in CI can change without this line changing. A compromised upstream can repoint the tag and execute arbitrary code with access to this repository's secrets and GITHUB_TOKEN (tj-actions/changed-files, March 2025). Pin to the full 40-character commit SHA with the resolved version in a trailing comment, e.g. uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2. Prefer deputy pin --ecosystems github-actions, which resolves the ref, writes a version comment that reflects the most specific ref actually pointing at that commit, and verifies the SHA is reachable from a real branch upstream. That last check matters: pinning alone does not detect imposter or dangling commits, and this rule only sees the shape of the ref, never its provenance. Reusable workflow calls (owner/repo/.github/workflows/x.yml@ref) run with the same trust as actions and are pinned the same way. Not reported, by campaign policy: temporalio/* refs (first-party, pinned by internal process), local ./ actions, self-repository $/ refs (resolve to the running commit, so they are already pin-equivalent), and docker:// images (pinned by digest as a separate ecosystem).


Suppress this finding

Add a suppression comment on the line before:

# noopengrep: security.gha.unpinned-action

permissions:
contents: read
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.gha.unpinned-action (WARNING)

Unpinned action reference actions/checkout@v4: this uses: resolves a mutable ref (tag or branch), so the code that runs in CI can change without this line changing. A compromised upstream can repoint the tag and execute arbitrary code with access to this repository's secrets and GITHUB_TOKEN (tj-actions/changed-files, March 2025). Pin to the full 40-character commit SHA with the resolved version in a trailing comment, e.g. uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2. Prefer deputy pin --ecosystems github-actions, which resolves the ref, writes a version comment that reflects the most specific ref actually pointing at that commit, and verifies the SHA is reachable from a real branch upstream. That last check matters: pinning alone does not detect imposter or dangling commits, and this rule only sees the shape of the ref, never its provenance. Reusable workflow calls (owner/repo/.github/workflows/x.yml@ref) run with the same trust as actions and are pinned the same way. Not reported, by campaign policy: temporalio/* refs (first-party, pinned by internal process), local ./ actions, self-repository $/ refs (resolve to the running commit, so they are already pin-equivalent), and docker:// images (pinned by digest as a separate ecosystem).


Suppress this finding

Add a suppression comment on the line before:

# noopengrep: security.gha.unpinned-action

docker: ${{ steps.filter.outputs.docker }}
track: ${{ steps.filter.outputs.track }}
steps:
- uses: actions/checkout@v4

@github-actions github-actions Bot Aug 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.gha.unpinned-action (WARNING)

Unpinned action reference actions/checkout@v4: this uses: resolves a mutable ref (tag or branch), so the code that runs in CI can change without this line changing. A compromised upstream can repoint the tag and execute arbitrary code with access to this repository's secrets and GITHUB_TOKEN (tj-actions/changed-files, March 2025). Pin to the full 40-character commit SHA with the resolved version in a trailing comment, e.g. uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2. Prefer deputy pin --ecosystems github-actions, which resolves the ref, writes a version comment that reflects the most specific ref actually pointing at that commit, and verifies the SHA is reachable from a real branch upstream. That last check matters: pinning alone does not detect imposter or dangling commits, and this rule only sees the shape of the ref, never its provenance. Reusable workflow calls (owner/repo/.github/workflows/x.yml@ref) run with the same trust as actions and are pinned the same way. Not reported, by campaign policy: temporalio/* refs (first-party, pinned by internal process), local ./ actions, self-repository $/ refs (resolve to the running commit, so they are already pin-equivalent), and docker:// images (pinned by digest as a separate ecosystem).

Fixed in 5b2afff

track: ${{ steps.filter.outputs.track }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3

@github-actions github-actions Bot Aug 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.gha.unpinned-action (WARNING)

Unpinned action reference dorny/paths-filter@v3: this uses: resolves a mutable ref (tag or branch), so the code that runs in CI can change without this line changing. A compromised upstream can repoint the tag and execute arbitrary code with access to this repository's secrets and GITHUB_TOKEN (tj-actions/changed-files, March 2025). Pin to the full 40-character commit SHA with the resolved version in a trailing comment, e.g. uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2. Prefer deputy pin --ecosystems github-actions, which resolves the ref, writes a version comment that reflects the most specific ref actually pointing at that commit, and verifies the SHA is reachable from a real branch upstream. That last check matters: pinning alone does not detect imposter or dangling commits, and this rule only sees the shape of the ref, never its provenance. Reusable workflow calls (owner/repo/.github/workflows/x.yml@ref) run with the same trust as actions and are pinned the same way. Not reported, by campaign policy: temporalio/* refs (first-party, pinned by internal process), local ./ actions, self-repository $/ refs (resolve to the running commit, so they are already pin-equivalent), and docker:// images (pinned by digest as a separate ecosystem).

Fixed in 5b2afff

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Kotlin-based, instructor-led Instruqt workshop track that teaches decoupling a monolith into Payments + Compliance services using Temporal Nexus, including a sandbox Docker image, track/challenge definitions, and CI to build/publish the track.

Changes:

  • Introduces kotlin/decouple-monolith/{exercise,solution} Gradle (KTS) projects for the lab content.
  • Adds an Instruqt track under kotlin/instruqt/ with five challenges plus setup/cleanup/solve/check scripts.
  • Adds a Kotlin sandbox Docker image and GitHub Actions workflows to build/push the image and publish the track.

Reviewed changes

Copilot reviewed 91 out of 94 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
.github/workflows/_track-ci.yml Reusable CI to build sandbox image and push track
.github/workflows/build-and-push-kotlin.yml Kotlin track CI caller, scoped to kotlin/**
.gitignore Adds Gradle and VS Code output ignores
README.md Documents Kotlin Instruqt track and workflow
kotlin/decouple-monolith/exercise/build.gradle.kts Gradle build + entrypoints for exercise
kotlin/decouple-monolith/exercise/gradle/wrapper/gradle-wrapper.properties Exercise Gradle wrapper config
kotlin/decouple-monolith/exercise/gradlew Exercise Gradle wrapper script (POSIX)
kotlin/decouple-monolith/exercise/gradlew.bat Exercise Gradle wrapper script (Windows)
kotlin/decouple-monolith/exercise/settings.gradle.kts Exercise project name
kotlin/decouple-monolith/exercise/src/main/kotlin/compliance/ComplianceChecker.kt Exercise compliance rules engine
kotlin/decouple-monolith/exercise/src/main/kotlin/compliance/domain/ComplianceRequest.kt Exercise compliance request model
kotlin/decouple-monolith/exercise/src/main/kotlin/compliance/domain/ComplianceResult.kt Exercise compliance result model
kotlin/decouple-monolith/exercise/src/main/kotlin/compliance/temporal/ComplianceNexusServiceImpl.kt Exercise Nexus handler TODOs
kotlin/decouple-monolith/exercise/src/main/kotlin/compliance/temporal/ComplianceWorkerApp.kt Exercise compliance worker TODOs
kotlin/decouple-monolith/exercise/src/main/kotlin/compliance/temporal/activity/ComplianceActivity.kt Exercise compliance activity contract
kotlin/decouple-monolith/exercise/src/main/kotlin/compliance/temporal/activity/ComplianceActivityImpl.kt Exercise compliance activity impl
kotlin/decouple-monolith/exercise/src/main/kotlin/compliance/temporal/workflow/ComplianceWorkflow.kt Exercise compliance workflow interface
kotlin/decouple-monolith/exercise/src/main/kotlin/compliance/temporal/workflow/ComplianceWorkflowImpl.kt Exercise compliance workflow implementation
kotlin/decouple-monolith/exercise/src/main/kotlin/payments/PaymentGateway.kt Exercise payment gateway simulation
kotlin/decouple-monolith/exercise/src/main/kotlin/payments/Shared.kt Exercise shared payment constants
kotlin/decouple-monolith/exercise/src/main/kotlin/payments/domain/PaymentRequest.kt Exercise payment request model
kotlin/decouple-monolith/exercise/src/main/kotlin/payments/domain/PaymentResult.kt Exercise payment result model
kotlin/decouple-monolith/exercise/src/main/kotlin/payments/temporal/PaymentProcessingWorkflow.kt Exercise payment workflow interface
kotlin/decouple-monolith/exercise/src/main/kotlin/payments/temporal/PaymentProcessingWorkflowImpl.kt Exercise payment workflow TODOs (Activity→Nexus)
kotlin/decouple-monolith/exercise/src/main/kotlin/payments/temporal/PaymentStarter.kt Exercise starter to run sample payments
kotlin/decouple-monolith/exercise/src/main/kotlin/payments/temporal/PaymentsWorkerApp.kt Exercise payments worker TODOs (endpoint mapping + decouple)
kotlin/decouple-monolith/exercise/src/main/kotlin/payments/temporal/ReviewCallerWorkflow.kt Exercise workflow to call submitReview via Nexus
kotlin/decouple-monolith/exercise/src/main/kotlin/payments/temporal/ReviewCallerWorkflowImpl.kt Exercise review caller workflow impl
kotlin/decouple-monolith/exercise/src/main/kotlin/payments/temporal/ReviewStarter.kt Exercise starter for human review demo
kotlin/decouple-monolith/exercise/src/main/kotlin/payments/temporal/activity/PaymentActivity.kt Exercise payment activity contract
kotlin/decouple-monolith/exercise/src/main/kotlin/payments/temporal/activity/PaymentActivityImpl.kt Exercise payment activity impl
kotlin/decouple-monolith/exercise/src/main/kotlin/shared/domain/ReviewRequest.kt Exercise review request model
kotlin/decouple-monolith/exercise/src/main/kotlin/shared/nexus/ComplianceNexusService.kt Exercise shared Nexus contract TODOs
kotlin/decouple-monolith/solution/build.gradle.kts Gradle build + entrypoints for solution
kotlin/decouple-monolith/solution/gradle/wrapper/gradle-wrapper.properties Solution Gradle wrapper config
kotlin/decouple-monolith/solution/gradlew Solution Gradle wrapper script (POSIX)
kotlin/decouple-monolith/solution/gradlew.bat Solution Gradle wrapper script (Windows)
kotlin/decouple-monolith/solution/settings.gradle.kts Solution project name
kotlin/decouple-monolith/solution/src/main/kotlin/compliance/ComplianceChecker.kt Solution compliance rules engine
kotlin/decouple-monolith/solution/src/main/kotlin/compliance/domain/ComplianceRequest.kt Solution compliance request model
kotlin/decouple-monolith/solution/src/main/kotlin/compliance/domain/ComplianceResult.kt Solution compliance result model
kotlin/decouple-monolith/solution/src/main/kotlin/compliance/temporal/ComplianceNexusServiceImpl.kt Solution Nexus handler implementation
kotlin/decouple-monolith/solution/src/main/kotlin/compliance/temporal/ComplianceWorkerApp.kt Solution compliance worker wiring
kotlin/decouple-monolith/solution/src/main/kotlin/compliance/temporal/activity/ComplianceActivity.kt Solution compliance activity contract
kotlin/decouple-monolith/solution/src/main/kotlin/compliance/temporal/activity/ComplianceActivityImpl.kt Solution compliance activity impl
kotlin/decouple-monolith/solution/src/main/kotlin/compliance/temporal/workflow/ComplianceWorkflow.kt Solution compliance workflow interface
kotlin/decouple-monolith/solution/src/main/kotlin/compliance/temporal/workflow/ComplianceWorkflowImpl.kt Solution compliance workflow implementation
kotlin/decouple-monolith/solution/src/main/kotlin/payments/PaymentGateway.kt Solution payment gateway simulation
kotlin/decouple-monolith/solution/src/main/kotlin/payments/Shared.kt Solution shared payment constants
kotlin/decouple-monolith/solution/src/main/kotlin/payments/domain/PaymentRequest.kt Solution payment request model
kotlin/decouple-monolith/solution/src/main/kotlin/payments/domain/PaymentResult.kt Solution payment result model
kotlin/decouple-monolith/solution/src/main/kotlin/payments/temporal/PaymentProcessingWorkflow.kt Solution payment workflow interface
kotlin/decouple-monolith/solution/src/main/kotlin/payments/temporal/PaymentProcessingWorkflowImpl.kt Solution payment workflow using Nexus
kotlin/decouple-monolith/solution/src/main/kotlin/payments/temporal/PaymentStarter.kt Solution starter with concurrent runs
kotlin/decouple-monolith/solution/src/main/kotlin/payments/temporal/PaymentsWorkerApp.kt Solution payments worker with endpoint mapping
kotlin/decouple-monolith/solution/src/main/kotlin/payments/temporal/ReviewCallerWorkflow.kt Solution workflow to call submitReview via Nexus
kotlin/decouple-monolith/solution/src/main/kotlin/payments/temporal/ReviewCallerWorkflowImpl.kt Solution review caller workflow impl
kotlin/decouple-monolith/solution/src/main/kotlin/payments/temporal/ReviewStarter.kt Solution starter for human review demo
kotlin/decouple-monolith/solution/src/main/kotlin/payments/temporal/activity/PaymentActivity.kt Solution payment activity contract
kotlin/decouple-monolith/solution/src/main/kotlin/payments/temporal/activity/PaymentActivityImpl.kt Solution payment activity impl
kotlin/decouple-monolith/solution/src/main/kotlin/shared/domain/ReviewRequest.kt Solution review request model
kotlin/decouple-monolith/solution/src/main/kotlin/shared/nexus/ComplianceNexusService.kt Solution shared Nexus contract
kotlin/instruqt/01-run-the-monolith/assignment.md Challenge 01 instructions
kotlin/instruqt/01-run-the-monolith/check-workshop Challenge 01 check script
kotlin/instruqt/01-run-the-monolith/cleanup-workshop Challenge 01 cleanup script
kotlin/instruqt/01-run-the-monolith/setup-workshop Challenge 01 setup script
kotlin/instruqt/01-run-the-monolith/solve-workshop Challenge 01 solve script
kotlin/instruqt/02-the-shared-contract/assignment.md Challenge 02 instructions
kotlin/instruqt/02-the-shared-contract/check-workshop Challenge 02 check script
kotlin/instruqt/02-the-shared-contract/cleanup-workshop Challenge 02 cleanup script
kotlin/instruqt/02-the-shared-contract/setup-workshop Challenge 02 setup script
kotlin/instruqt/02-the-shared-contract/solve-workshop Challenge 02 solve script
kotlin/instruqt/03-the-compliance-side/assignment.md Challenge 03 instructions
kotlin/instruqt/03-the-compliance-side/check-workshop Challenge 03 check script
kotlin/instruqt/03-the-compliance-side/cleanup-workshop Challenge 03 cleanup script
kotlin/instruqt/03-the-compliance-side/setup-workshop Challenge 03 setup script
kotlin/instruqt/03-the-compliance-side/solve-workshop Challenge 03 solve script
kotlin/instruqt/04-the-payments-side/assignment.md Challenge 04 instructions
kotlin/instruqt/04-the-payments-side/check-workshop Challenge 04 diagnostic check script
kotlin/instruqt/04-the-payments-side/cleanup-workshop Challenge 04 cleanup script
kotlin/instruqt/04-the-payments-side/setup-workshop Challenge 04 setup script
kotlin/instruqt/04-the-payments-side/solve-workshop Challenge 04 solve script
kotlin/instruqt/05-durability-and-human-review/assignment.md Challenge 05 instructions
kotlin/instruqt/05-durability-and-human-review/check-workshop Challenge 05 check script
kotlin/instruqt/05-durability-and-human-review/cleanup-workshop Challenge 05 cleanup script
kotlin/instruqt/05-durability-and-human-review/setup-workshop Challenge 05 setup script
kotlin/instruqt/05-durability-and-human-review/solve-workshop Challenge 05 solve script
kotlin/instruqt/config.yml Instruqt lab container config (image/memory/ports)
kotlin/instruqt/track.yml Instruqt track metadata
kotlin/instruqt/track_scripts/cleanup-workshop Track-level teardown script
kotlin/instruqt/track_scripts/setup-workshop Track-level bootstrap script
kotlin/sandbox/Dockerfile Workshop sandbox image build

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +33 to +47
if [ "${ATTEMPT:-0}" -gt 2 ]; then
fail-message "$WID is stuck retrying its Workflow task (attempt $ATTEMPT).

That is the signature of an Endpoint name that does not match. The Workflow is not
failing, it is retrying a command the server keeps rejecting.

Check the Payments Worker tab for:

INVALID_ARGUMENT: BadScheduleNexusOperationAttributes: endpoint \"...\" not found

Then confirm the name in PaymentsWorkerApp.kt matches the Endpoint you created:

temporal operator nexus endpoint list"
exit 1
fi
Comment thread README.md
│ └── solution/ Gradle KTS, complete
├── sandbox/Dockerfile Temurin 21 + Temporal CLI + pre-warmed Gradle cache
└── instruqt/
├── track.yml no challenges: block, challenges auto-discovered
Comment on lines +65 to +74
- uses: docker/build-push-action@v6
with:
context: ${{ inputs.track_dir }}
file: ${{ inputs.track_dir }}/sandbox/Dockerfile
push: true
platforms: linux/amd64
tags: |
${{ steps.tags.outputs.extra }}
${{ inputs.image }}:${{ github.sha }}

Comment thread kotlin/sandbox/Dockerfile
Comment on lines +32 to +35
ARG CODE_SERVER_VERSION=4.96.4
RUN curl -fsSL https://code-server.dev/install.sh | sh -s -- --version ${CODE_SERVER_VERSION}
RUN code-server --extensions-dir /root/.cs-extensions \
--install-extension mathiasfrohlich.kotlin
nadvolod and others added 13 commits August 12, 2026 10:38
…ight

Four fixes found by testing the live track, each with a distinct failure mode.

Declare 8233, 8080 and 8081 under `ports:`. Instruqt's proxy cannot reach an
undeclared port and the tab shows "Please wait" forever with no error. This was
the cause of the editor tabs failing, not anything in the container.

Pin the sandbox image by digest instead of :latest, so a cached older image
cannot boot in place of the one just pushed.

Start code-server before the Temporal dev server rather than after. The health
check downstream of it could exit non-zero, which took the editors down with a
slow Temporal start. That exit is also gone: it abandoned the rest of setup for
no benefit, and now warns and dumps the log instead.

Raise memory 4096 -> 8192. Measured 3.69 GiB with both Workers running, before
the starter even executes, so the old ceiling was marginal.

Also bakes code-server settings with workspace trust disabled. The editor tabs
are back on the native `type: code` editor for now, but code-server stays in the
image so re-enabling is a tab-only change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Assignment prose was restating what the code already said, and reaching for
metaphors where a direct sentence was shorter.

Stop enumerating TODOs in the assignments. Every file-editing step now reads
"open <file> and follow the TODO comments". The code owns the instructions; the
assignment owns why it matters. Counting the edits told learners nothing they
would not see on opening the file.

Drop the menu/kitchen metaphor. It came from the Java tutorial's quiz, which
builds up to it, and landed as a non-sequitur without that setup.

Rewrite the async/sync explanation for someone new to Temporal. "Two handlers,
two different shapes" became the slow one and the fast one, with the reason a
compliance check is slow (a person has to approve it) and what breaks if you
pick wrong (cut off at 10s, and every retry starts another check).

Name the code a TODO refers to. "Replace this Activity stub" did not say which
declaration, or what a stub is. TODOs 4a, 4b, 5a and 5c now quote the exact line
and show the shape of the replacement.

Remove the docs/Ask-AI guidance from the assignment. It duplicated the block in
ComplianceNexusService.kt, which is where a stuck learner is already looking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Worker registration TODO was the least clear thing in the lab. It said "this
registration carries no Endpoint mapping", which raises three questions and
answers none: which registration, what is an Endpoint mapping, and where does
the code go.

It now points at the line explicitly ("THE LINE TO CHANGE is the very next line
of code"), defines the term in one sentence (a pair: service name -> Endpoint
name), says why it belongs on the Worker and not in the Workflow, and shows the
full replacement with a single blank to fill in.

Also renumbers every TODO in the exercise to a plain sequence, 1 through 11, in
the order a learner meets them. The old scheme mixed 1a/1b/1c, 2a-2d, 3, 4a/4b
and 5, so a learner had to work out whether 4b came before 5. Merges the two
TODOs that sat on the same method into one, since annotating and implementing
are a single edit in practice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…adiness

Instruqt support diagnosed the blank editor tab. Setup declared code-server
"ready" using `curl -sf http://127.0.0.1:8080/`, which passes the moment the
HTTP listener binds: bare "/" 302-redirects and `curl -sf` treats a 3xx as
success. The workbench (extension host, workspace init) was still booting, so
the attendee's tab raced a half-started backend and rendered blank until a
manual refresh.

Their suggested fix polls /healthz for '"status":"ready"'. Measuring the live
endpoint shows code-server 4.96.4 never emits that: /healthz returns "expired"
then "alive", and "alive" only means an HTTP request arrived recently, which the
polling itself causes. Gating on it would never pass, adding 90s and a false
warning to every boot.

Same intent, signals that exist in this version. Wait until both are true:
  1. the exact URL the tab opens returns 200 (bare "/" still 302s at that point,
     which is the difference the old check missed)
  2. code-server logs "Extension host agent started"

Verified against the pinned image: setup returns with no warning, and both tab
URLs answer 200 the instant it does.

Exercise and Solution tabs go back to type: service on 8080/8081, so learners
get VS Code with Kotlin syntax highlighting instead of the native editor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The blank editor is a layout failure, not a proxy or readiness problem. The
browser console shows:

    ERR Unable to figure out browser width and height
    The Web Worker Extension Host did not start in 60s

Instruqt loads every service tab's iframe at challenge start, including tabs the
attendee cannot see. code-server cannot compute its layout in a 0x0 iframe, so
the workbench never finishes booting. Refreshing works because by then the tab
is visible and has real dimensions.

Two instances meant two hidden iframes racing that boot simultaneously. Now one
instance on 8080 rooted at /root/workshop, with both editor tabs selecting their
directory via ?folder=. That is exactly the shape temporal-ai-agents-python-v4
uses, which is the one Temporal track known to run code-server through this
proxy successfully.

Drops 8081 from the declared ports and frees about 200 MB (461 MB total now).

Verified on the pinned image: setup completes with no warning and both tab URLs
return 200 from the single instance. The iframe sizing itself cannot be
reproduced outside a real Instruqt sandbox.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two bugs, both visible as challenge 01 printing the decoupled Worker banner
instead of the monolith one.

Skip permanently solved the sandbox. The solve scripts for challenges 04 and 05
do `rm -rf exercise/src && cp -R solution/src exercise/src`, and nothing ever put
the TODOs back. One Skip and the exercise was gone for the life of that sandbox:
challenge 01 showed decoupled output, and every TODO was pre-answered before the
attendee reached it.

Challenge 01's setup now restores the tree from the baked copy, but only when it
has NO TODO markers left, which means it was overwritten wholesale. A pristine or
partly-completed tree is untouched, so an attendee who navigates back to
challenge 01 does not lose work. Verified both paths.

The pinned image was also stale. Pinning by digest fixed cached-:latest problems
and introduced the opposite one: six subsequent commits to exercise source never
reached the sandbox, so attendees were reading the old 4a/4b/5a TODO numbering
instead of 1..11. Rebuilt and re-pinned, and the README now states the rule that
any change under decouple-monolith/ needs a rebuild plus a re-pin, while
instruqt/ changes are push-only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TODO count, container memory, declared ports, and the editor tabs, which are
code-server service tabs now rather than the native code editor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Instruqt renders the first tab in the list as the active one, and only an active
tab's iframe has real dimensions at load. code-server cannot compute its layout
in a 0x0 iframe, so the workbench never finished booting and the tab stayed blank
until a manual refresh. The browser console said so directly:

    ERR Unable to figure out browser width and height
    The Web Worker Extension Host did not start in 60s

Exercise moves from position 1 to position 0, Temporal UI takes position 1.

tab-N is a zero-indexed position, not an id, so all 22 button references were
remapped in the same change and every button label was checked against the tab it
now targets. Verified: no mismatches, all references in range, and the server
kept the new order with all 30 tab ids intact.

Known limitation: Solution is still position 5 and still boots hidden, so it may
still need one refresh. Only one tab can be first, so reordering cannot fix both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… cost us

Adds an interactive architecture diagram as a seventh tab, showing challenge 01's
monolith. Colour carries the argument: Payments code in blue, Compliance code in
amber, and the amber blocks sit inside the Payments Worker box on the Payments
Task Queue. Beside it, an empty compliance-namespace, which fills up in challenge
04. Every block shows its file, its method signature and a Kotlin-highlighted
snippet, with a detail panel, legend filtering and zoom.

Served by jwebserver, which ships with the JDK, so the Temurin image needs no
extra package. It has no Python. Diagram lives in kotlin/diagrams/ because
Instruqt parses every file inside a challenge folder as a lifecycle script.

Appended at position 6, so all 22 existing tab-N buttons keep working. Verified
by label match: 22 refs, 0 mismatches, 35 of 35 tab ids assigned.

Also adds INSTRUQT.md. This track burned a lot of time on failures that each
looked like something else: undeclared ports reading as a hung service, a hidden
0x0 iframe reading as a broken proxy, a digest pin quietly serving code from six
commits earlier, and a piped docker build reporting success while failing. All of
it is written down symptom-first, including the fix that finally made the editor
tab work: make it the first tab so its iframe has real dimensions at load.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e legend

Three problems with the first version, all from the screenshots.

Boxes overlapped. Every node had a hardcoded top and a min-height guessed by
hand, so any block whose snippet ran taller grew straight through the one below
it, and the step order became unreadable. Nothing is hardcoded now: nodes are
rendered, measured, then stacked with a fixed gap from the real height above.
Zones size to their measured columns, and the process outline sizes to the zones.
Editing a snippet re-flows the layout instead of colliding.

The two teams were not obvious. They are now filled, bordered, labelled regions,
PAYMENTS TEAM in blue and COMPLIANCE TEAM in amber, sitting side by side inside
one dashed process outline. That single picture is the whole argument: two teams,
one box around both. Each zone says what it owns, and the Compliance one says it
cannot deploy what it owns.

There was no way to walk the flow. Added a step player: arrows, a step counter, a
phase pill and narration, with arrow-key support. Eight steps from Worker
registration through the step-2 boundary crossing to the empty
compliance-namespace. Each step lights its nodes and edges and fades the rest.

Removed the legend. Zoom, Fit and Reset view are the only controls left. The team
zones explain the colours, so a filter legend was noise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stepping through the diagram moved the arrows, so you had to chase them with the
mouse between clicks. Three separate causes, all pinned.

Vertical: the narration grew from one line to two depending on caption length,
and the bar grew with it. The bar now has a fixed height and the narration
reserves exactly two lines whatever the text.

Horizontal: the play button changes text between "Walk the flow" and "Restart",
which shifted the arrows sideways on every start and restart. It now has a fixed
width. The step counter got one too.

The phase pill keeps its space when hidden, so entering and leaving flow mode
does not reflow either.

Also checked that captions actually fit two lines rather than assuming it. The
longest was 156 characters, which wraps to three lines on a narrow lab pane and
would have been clipped mid-word. Trimmed the two worst to 129 and added
line-clamp so any overflow ends in an ellipsis instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every requirement in this section is a defect we shipped and had to fix, not a
style preference, so it is written that way: no overlapping boxes (compute the
layout, never hardcode a top), a control bar that cannot move between steps
(fixed height, two-line narration, fixed-width buttons), step arrows rather than
a static picture, no legend, labelled regions per actor, and file name plus
method signature plus language-specific highlighting on every block.

Ends with a copy-pasteable prompt with four bracketed slots, so the next diagram
starts from the finished set of constraints instead of rediscovering them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sandbox image lives at ghcr.io/nadvolod/edu-nexus-kotlin-sandbox, and that is
intentional. This repo's default GITHUB_TOKEN cannot write to a personal GHCR
namespace, so a build job here would fail on every single merge and leave CI
permanently red for a reason nobody can fix from inside the repo.

So CI now does the one thing it can do: push the track definition. The reusable
pipeline blocks push-track only when the build FAILED, not when it was skipped,
so turning the build off leaves the publish path intact.

Also narrows the trigger to kotlin/instruqt/**. Because the image is pinned by
digest, a source change reaches attendees only once the re-pin is committed, and
that re-pin edits config.yml under instruqt/. Watching sandbox/ and
decouple-monolith/ would fire a track push that changes nothing.

The manual rebuild and re-pin recipe is in the workflow header and the README.
Moving the image to ghcr.io/temporalio/... later is a one-line change back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants