feat(kotlin): Nexus decouple-a-monolith workshop and Instruqt track - #9
Conversation
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>
| packages: write | ||
| steps: | ||
| - name: Checkout repo | ||
| uses: actions/checkout@v4 |
There was a problem hiding this comment.
Opengrep — security.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 |
There was a problem hiding this comment.
Opengrep — security.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 |
There was a problem hiding this comment.
Opengrep — security.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 |
There was a problem hiding this comment.
Opengrep — security.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 |
There was a problem hiding this comment.
Opengrep — security.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 |
There was a problem hiding this comment.
Opengrep — security.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
There was a problem hiding this comment.
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.
| 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 |
| │ └── solution/ Gradle KTS, complete | ||
| ├── sandbox/Dockerfile Temurin 21 + Temporal CLI + pre-warmed Gradle cache | ||
| └── instruqt/ | ||
| ├── track.yml no challenges: block, challenges auto-discovered |
| - 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 }} | ||
|
|
| 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 |
…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>
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 openkotlin/decouple-monolith/solution— the same project, completekotlin/sandbox/Dockerfile— Temurin 21, Temporal CLI, code-server, warm Gradle cachekotlin/diagrams/— interactive monolith architecture diagramkotlin/instruqt/— track definition and five challengesINSTRUQT.md— what this track cost us, written symptom-first.github/workflows/— reusable track CI plus a Kotlin caller scoped tokotlin/**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.
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: codetab, solearners 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-namespacethat fills up in challenge 4.Deliberate divergences from
java/PaymentStarterstarts the three Workflows without blocking. The Java starteris a sequential blocking loop, which deadlocks once TXN-B parks for human
review: TXN-C would never start.
println. The Java exercise callsWorkflow.getLoggerfrommain, which throws off a Workflow thread.catch (e: Exception)was swallowingCanceledFailure, so a cancelled Workflow reported success.executePaymentis idempotent. It derived the confirmation number from theclock, so each retry minted a different one, in a payments demo.
Reviewer notes
Merging publishes the track.
build-and-push-kotlin.ymlrunsinstruqt track push --forceon push tomaintouchingkotlin/instruqt/**.It needs a repo secret named
INSTRUQT_TOKEN. The track ismaintenance: 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_TOKENcannot write to, so a build job wouldfail 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/...lateris 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; onlykotlin/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.shstops checking tab references after the first push. Itscounter only matches entries starting with
- title:, but pushed tabs start with- id:. Worth a one-line fix intemporal-edu-skills. Buttons here were verifiedby 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.