slack
── WHY THIS MODULE EXISTS ──────────────────────────────────────────────────This code used to live vendored byte-for-byte in three modules (`pacha`,
`pacha-api`, `pacha-site`), 363 lines each, and the only thing keeping the
copies equal was an md5 recorded in `.engine-parity`. On 2026-09-04 that guard
found a real divergence that had been there for months: `pacha` carried one
extra line nobody had mirrored. Twelve months of a convention held up by a note.
── WHY IT IS A CLIENT AND NOT AN ORCHESTRATOR ──────────────────────────────
The vendored version exposed one `run(title, meta, items, …, body)` that took
the whole pipeline as an async CALLBACK. That shape cannot cross a module
boundary: a Dagger function is a GraphQL call, and a TypeScript closure is not
serialisable. So the split is:
· here everything that is genuinely the same everywhere — Block Kit
rendering, the trend read, the breakdown, the HTTP calls.
· in each repo a ~40-line loop holding `items`, `startedAt`, `metrics` and
`ts`, which is where the orchestration actually differs.
Duplication therefore drops from 363 lines to ~40, not to zero. Claiming zero
would be claiming something this interface cannot deliver.
── WHAT IS DELIBERATELY NOT PORTED ─────────────────────────────────────────
`approveProd` — the production approval gate that used to poll Slack from
inside Dagger. It was retired on 2026-08-22 (ADR-0003 §2.3) and replaced by a
GitHub deployment protection rule served by the `deploy-gate` service, because
polling held a self-hosted runner busy for up to 30 minutes waiting for a
click. Verified on 2026-09-05: `ctx.gate` is called by NO pipeline in any of
the three repos, so it is dead code in all of them.
Porting it here would take that retired second path to production and make it
freshly callable by four repos at once — which is exactly the debt ADR-0003
§7.1 named and the 2026-08-22 migration closed. UN SOLO CAMINO A PRODUCCIÓN.
── COMPLEX ARGUMENTS TRAVEL AS JSON STRINGS ────────────────────────────────
Dagger's TypeScript SDK exposes structural types poorly across a module
boundary, and a shape mismatch there fails at call time with an unreadable
error. Every non-scalar crosses as a JSON string, and the shapes are documented
on each function. The caller does `JSON.stringify`; this module parses and
validates.
Installation
dagger install github.com/wildbitca/daggerverse/slack@v0.1.0Entrypoint
Return Type
Slack Example
dagger -m github.com/wildbitca/daggerverse/slack@082b6b441ec9bbe79c0d9e8db3d244d9679197c8 call \
func (m *MyModule) Example() *dagger.Slack {
return dag.
Slack()
}@function
def example() -> dagger.Slack:
return (
dag.slack()
)@func()
example(): Slack {
return dag
.slack()
}Types
Slack 🔗
post() 🔗
Post or edit the progress card. Returns the message ts.
With an empty ts it posts (chat.postMessage); with one it edits in place
(chat.update). ONE message per run, edited — not a thread of new messages.
Best-effort by contract: a Slack outage logs and returns the ts it was
given, and NEVER brings the pipeline down. A notification channel that can
fail the build is a notification channel that gets removed.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| token | Secret ! | - | No description provided |
| channel | String ! | - | No description provided |
| body | String ! | - | JSON of the Slack message body — what |
| ts | String ! | "" | message to edit; empty posts a new one. |
Example
dagger -m github.com/wildbitca/daggerverse/slack@082b6b441ec9bbe79c0d9e8db3d244d9679197c8 call \
post --token env:MYSECRET --channel string --body string --ts stringfunc (m *MyModule) Example(ctx context.Context, token *dagger.Secret, channel string, body string, ts string) string {
return dag.
Slack().
Post(ctx, token, channel, body, ts)
}@function
async def example(token: dagger.Secret, channel: str, body: str, ts: str) -> str:
return await (
dag.slack()
.post(token, channel, body, ts)
)@func()
async example(token: Secret, channel: string, body: string, ts: string): Promise<string> {
return dag
.slack()
.post(token, channel, body, ts)
}render() 🔗
Render the progress card. Pure — no container, no network.
The returned JSON carries metadata.event_type + event_payload, which is
what readTrend reads on the NEXT run: Slack itself is the trend store, so
there is no database to run.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| title | String ! | - | No description provided |
| status | String ! | - | running | ok | fail |
| meta | String ! | - | JSON Meta |
| items | String ! | - | JSON Item[] |
| elapsedMs | Integer ! | - | milliseconds since the run started, measured by the caller |
| eventType | String ! | - | trend series key — MUST match /^[a-z0-9_]+$/ (Slack rejects the rest) |
| metrics | String ! | "" | JSON TestMetrics, or “” when the suite has not reported yet |
| trend | String ! | "" | JSON Trend from the previous run, or “” |
Example
dagger -m github.com/wildbitca/daggerverse/slack@082b6b441ec9bbe79c0d9e8db3d244d9679197c8 call \
render --title string --status string --meta string --items string --elapsed-ms integer --event-type string --metrics string --trend stringfunc (m *MyModule) Example(ctx context.Context, title string, status string, meta string, items string, elapsedMs int, eventType string, metrics string, trend string) string {
return dag.
Slack().
Render(ctx, title, status, meta, items, elapsedMs, eventType, metrics, trend)
}@function
async def example(title: str, status: str, meta: str, items: str, elapsedms: int, eventtype: str, metrics: str, trend: str) -> str:
return await (
dag.slack()
.render(title, status, meta, items, elapsedms, eventtype, metrics, trend)
)@func()
async example(title: string, status: string, meta: string, items: string, elapsedMs: number, eventType: string, metrics: string, trend: string): Promise<string> {
return dag
.slack()
.render(title, status, meta, items, elapsedMs, eventType, metrics, trend)
}readTrend() 🔗
Read the previous run’s metrics from Slack — the trend store is Slack itself.
Matches on event_type AND repo: event_type alone was not enough once
two lanes of the same repo shared a series, and the comparison silently
compared a five-emulator e2e against a Simulator build and printed a
perfectly plausible, false “↑12m vs anterior”.
cacheBust MUST vary per run (use the run id). Without it Dagger serves the
previous exec from cache and every run reads the same stale trend — a network
read cached is a network read that did not happen (Gotcha 2).
Best-effort: returns “” on any failure. No trend is a missing line on a card; a hard failure here would be a pipeline down because a chat app was slow.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| token | Secret ! | - | No description provided |
| channel | String ! | - | No description provided |
| eventType | String ! | - | No description provided |
| repo | String ! | - | No description provided |
| cacheBust | String ! | - | No description provided |
Example
dagger -m github.com/wildbitca/daggerverse/slack@082b6b441ec9bbe79c0d9e8db3d244d9679197c8 call \
read-trend --token env:MYSECRET --channel string --event-type string --repo string --cache-bust stringfunc (m *MyModule) Example(ctx context.Context, token *dagger.Secret, channel string, eventType string, repo string, cacheBust string) string {
return dag.
Slack().
Readtrend(ctx, token, channel, eventType, repo, cacheBust)
}@function
async def example(token: dagger.Secret, channel: str, eventtype: str, repo: str, cachebust: str) -> str:
return await (
dag.slack()
.readtrend(token, channel, eventtype, repo, cachebust)
)@func()
async example(token: Secret, channel: string, eventType: string, repo: string, cacheBust: string): Promise<string> {
return dag
.slack()
.readTrend(token, channel, eventType, repo, cacheBust)
}breakdown() 🔗
The closing detail, in the card’s THREAD: duration per step, the test table and the trend. Posted on ok AND on fail — a run that failed is the one whose detail somebody actually needs.
In the thread and not as a second channel message: the card is already where this run is being watched, and a loose message forces pairing them by eye.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| token | Secret ! | - | No description provided |
| channel | String ! | - | No description provided |
| threadTs | String ! | - | No description provided |
| status | String ! | - | No description provided |
| items | String ! | - | No description provided |
| elapsedMs | Integer ! | - | No description provided |
| metrics | String ! | "" | No description provided |
| trend | String ! | "" | No description provided |
Example
dagger -m github.com/wildbitca/daggerverse/slack@082b6b441ec9bbe79c0d9e8db3d244d9679197c8 call \
breakdown --token env:MYSECRET --channel string --thread-ts string --status string --items string --elapsed-ms integer --metrics string --trend stringfunc (m *MyModule) Example(ctx context.Context, token *dagger.Secret, channel string, threadTs string, status string, items string, elapsedMs int, metrics string, trend string) string {
return dag.
Slack().
Breakdown(ctx, token, channel, threadTs, status, items, elapsedMs, metrics, trend)
}@function
async def example(token: dagger.Secret, channel: str, threadts: str, status: str, items: str, elapsedms: int, metrics: str, trend: str) -> str:
return await (
dag.slack()
.breakdown(token, channel, threadts, status, items, elapsedms, metrics, trend)
)@func()
async example(token: Secret, channel: string, threadTs: string, status: string, items: string, elapsedMs: number, metrics: string, trend: string): Promise<string> {
return dag
.slack()
.breakdown(token, channel, threadTs, status, items, elapsedMs, metrics, trend)
}threadReply() 🔗
Reply in the card’s thread. The per-repo detail (which flows are red, which .dart file broke) is posted with this — the engine knows nothing about flows.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| token | Secret ! | - | No description provided |
| channel | String ! | - | No description provided |
| threadTs | String ! | - | No description provided |
| text | String ! | - | No description provided |
| blocks | String ! | "" | JSON of a Block Kit array, or “” for a plain-text reply. |
Example
dagger -m github.com/wildbitca/daggerverse/slack@082b6b441ec9bbe79c0d9e8db3d244d9679197c8 call \
thread-reply --token env:MYSECRET --channel string --thread-ts string --text string --blocks stringfunc (m *MyModule) Example(ctx context.Context, token *dagger.Secret, channel string, threadTs string, text string, blocks string) string {
return dag.
Slack().
Threadreply(ctx, token, channel, threadTs, text, blocks)
}@function
async def example(token: dagger.Secret, channel: str, threadts: str, text: str, blocks: str) -> str:
return await (
dag.slack()
.threadreply(token, channel, threadts, text, blocks)
)@func()
async example(token: Secret, channel: string, threadTs: string, text: string, blocks: string): Promise<string> {
return dag
.slack()
.threadReply(token, channel, threadTs, text, blocks)
}failureDetail() 🔗
The error block a failed run posts in its thread before the breakdown.
Truncates from the END: the last 2600 characters of a stack trace are the ones that say what broke; the first 2600 are the ones that say the build started.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| token | Secret ! | - | No description provided |
| channel | String ! | - | No description provided |
| threadTs | String ! | - | No description provided |
| stage | String ! | - | No description provided |
| detail | String ! | - | No description provided |
Example
dagger -m github.com/wildbitca/daggerverse/slack@082b6b441ec9bbe79c0d9e8db3d244d9679197c8 call \
failure-detail --token env:MYSECRET --channel string --thread-ts string --stage string --detail stringfunc (m *MyModule) Example(ctx context.Context, token *dagger.Secret, channel string, threadTs string, stage string, detail string) string {
return dag.
Slack().
Failuredetail(ctx, token, channel, threadTs, stage, detail)
}@function
async def example(token: dagger.Secret, channel: str, threadts: str, stage: str, detail: str) -> str:
return await (
dag.slack()
.failuredetail(token, channel, threadts, stage, detail)
)@func()
async example(token: Secret, channel: string, threadTs: string, stage: string, detail: string): Promise<string> {
return dag
.slack()
.failureDetail(token, channel, threadTs, stage, detail)
}