schema: 1
name: Release notes digest
id: release-digest
description: |
Termorq Learn capstone. Every runnable block type and all seven languages,
started by an HTTP request with `{"changes": ["feat: ...", "fix: ..."]}`.
folder: ~/src/release-digest
timeout_secs: 300
inputs:
changes:
type: array
items:
type: string
env:
DIGEST_TITLE: Release notes digest
triggers:
enabled: true
manual: true
selected: http
http: true
blocks:
# 1 · Python: validate the request body
- id: validate
name: Validate input
kind: code
language: python
uses: [inputs.changes]
input_sample: |-
{"inputs":{"changes":["feat: dark mode","fix: crash on save","feat!: drop v1 API"]}}
body: |
import json, os, sys
with open(os.environ["TERMORQ_INPUT"], encoding="utf-8") as f:
data = json.load(f)
changes = data["inputs"]["changes"]
if not isinstance(changes, list) or not changes:
raise ValueError("inputs.changes must be a non-empty list")
if not all(isinstance(c, str) and c.strip() for c in changes):
raise ValueError("every change must be a non-empty string")
items = [c.strip() for c in changes][:50]
print(f"validated {len(items)} changes", file=sys.stderr)
print(json.dumps({"items": items, "total": len(items)}))
outputs:
type: object
properties:
items:
type: array
items:
type: string
total:
type: number
# 2 · Loop: classify (Bash) and score (PowerShell) every change
- id: each_change
name: Each change
kind: loop
for_each: steps.validate.items
max: 50
body:
- id: classify
name: Classify
kind: code
language: bash
uses: [loop.item]
body: |
set -euo pipefail
jq -c '
.loop.item as $t
| {title: $t,
kind: (if ($t | test("^(breaking|[a-z]+!:)"; "i")) then "breaking"
elif ($t | test("^feat"; "i")) then "feature"
elif ($t | test("^fix"; "i")) then "fix"
else "other" end)}' "$TERMORQ_INPUT"
outputs:
type: object
properties:
title:
type: string
kind:
type: string
enum: [breaking, feature, fix, other]
- id: score
name: Score
kind: code
language: powershell
uses: [steps.classify.kind, loop.index]
body: |
$ErrorActionPreference = 'Stop'
$d = Get-Content -Raw -LiteralPath $env:TERMORQ_INPUT | ConvertFrom-Json
$weights = @{ breaking = 5; feature = 3; fix = 2; other = 1 }
$kind = [string]$d.steps.classify.kind
@{ weight = $weights[$kind]; position = [int]$d.loop.index } | ConvertTo-Json -Compress
outputs:
type: object
properties:
weight:
type: number
position:
type: number
# 3 · C#: tally the loop results
- id: tally
name: Tally
kind: code
language: csharp
uses: [steps.each_change.results]
body: |
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;
var path = Environment.GetEnvironmentVariable("TERMORQ_INPUT")
?? throw new InvalidOperationException("TERMORQ_INPUT is missing");
using var doc = JsonDocument.Parse(File.ReadAllText(path));
var results = doc.RootElement.GetProperty("steps").GetProperty("each_change").GetProperty("results");
int breaking = 0, features = 0, fixes = 0;
double score = 0;
foreach (var pass in results.EnumerateArray())
{
var kind = pass.GetProperty("classify").GetProperty("kind").GetString();
if (kind == "breaking") breaking++;
else if (kind == "feature") features++;
else if (kind == "fix") fixes++;
score += pass.GetProperty("score").GetProperty("weight").GetDouble();
}
Console.WriteLine(new JsonObject
{
["breaking"] = breaking,
["features"] = features,
["fixes"] = fixes,
["score"] = score,
}.ToJsonString());
outputs:
type: object
properties:
breaking:
type: number
features:
type: number
fixes:
type: number
score:
type: number
# 4 · Rust: checksum of the validated items (std only, no Cargo)
- id: checksum
name: Checksum
kind: code
language: rust
uses: [steps.validate.items]
body: |
use std::{env, error::Error, fs};
fn main() -> Result<(), Box<dyn Error>> {
let bytes = fs::read(env::var("TERMORQ_INPUT")?)?;
// FNV-1a, 64-bit
let mut hash: u64 = 0xcbf29ce484222325;
for b in &bytes {
hash ^= *b as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
println!("{{\"checksum\":\"{:016x}\",\"bytes\":{}}}", hash, bytes.len());
Ok(())
}
outputs:
type: object
properties:
checksum:
type: string
bytes:
type: number
# 5 · JavaScript: format Markdown and a JSON-safe request body
- id: format
name: Format Markdown
kind: code
language: javascript
uses:
- steps.each_change.results
- steps.tally.breaking
- steps.tally.score
- steps.checksum.checksum
body: |
import { readFileSync } from "node:fs";
const { steps } = JSON.parse(readFileSync(process.env.TERMORQ_INPUT, "utf8"));
const icons = { breaking: "💥", feature: "✨", fix: "🐛", other: "•" };
const lines = steps.each_change.results.map(
(pass) => `- ${icons[pass.classify.kind]} ${pass.classify.title} (weight ${pass.score.weight})`
);
const markdown = [
`## ${process.env.DIGEST_TITLE}`,
"",
...lines,
"",
`Breaking: ${steps.tally.breaking} · Score: ${steps.tally.score} · Checksum: ${steps.checksum.checksum}`,
].join("\n");
console.log(JSON.stringify({ markdown, request_json: JSON.stringify({ message: markdown }) }));
outputs:
type: object
properties:
markdown:
type: string
request_json:
type: string
# 6 · Code file (TypeScript): stamp the digest and save it as an artifact
- id: stamp
name: Stamp digest
kind: code_file
path: scripts/stamp.ts
uses: [steps.format.markdown]
outputs:
type: object
properties:
stamped:
type: string
file:
type: string
# 7 · HTTP (offline mock): "publish" the digest
- id: publish
name: Publish
kind: http
method: POST
url: mock://digest
body: "{{ steps.format.request_json }}"
outputs:
type: object
properties:
path:
type: string
mock:
type: boolean
# 8 · MCP: post the digest through a saved tool
- id: post
name: Post via MCP
kind: mcp
server: Demo tools
tool: echo
arguments: "{{ steps.format.request_json }}"
outputs:
type: object
properties:
content:
type: array
# 9 · Decision: warn only when something is breaking
- id: gate
name: Breaking?
kind: decision
branches:
- when: steps.tally.breaking > 0
goto: ai_warn
- else: true
goto: quiet
# 10 · AI: write an upgrade warning
- id: ai_warn
name: Upgrade warning
kind: ai
connection: Summary model
system: 'Return a single JSON object with a string property named "summary".'
prompt: |
These release notes contain breaking changes. Write a two-sentence upgrade warning.
{{ steps.stamp.stamped }}
max_tokens: 200
outputs:
type: object
properties:
summary:
type: string
then: done
# 11 · Python: the quiet route
- id: quiet
name: All clear
kind: code
language: python
uses: [steps.validate.total]
body: |
import json, os
with open(os.environ["TERMORQ_INPUT"], encoding="utf-8") as f:
total = json.load(f)["steps"]["validate"]["total"]
print(json.dumps({"summary": f"No breaking changes in {total} items."}))
outputs:
type: object
properties:
summary:
type: string
then: done
# 12 · Join
- id: done
name: Done
kind: http
method: GET
url: mock://done