Dagger
Search

runcard

does not own.

── WHY THIS MODULE EXISTS ──────────────────────────────────────────────────
`slack` is a client: it renders and posts a card, and its header says the
loop that keeps the card current belongs to each repo. That held while every
pipeline was ONE `dagger call` that could carry the loop inside it. It stopped
holding once pipelines became many jobs on many runners: no dagger call spans
them, so each repo would need the same ~200-line watcher. `pacha/app` wrote it
first (`pipelineCardLive`, 2026-09-16); this is that prototype, shared.

── FOUR SHAPES, ONE CARD LOOK ──────────────────────────────────────────────
· `watch` multi-job workflows. A job with no `needs` polls the run's jobs
through the GitHub API and rewrites the card as rows change.
· `refresh` the same card, event-driven: ONE pass, no polling. Jobs call it
when they finish (`needs` + `if: always()`), each pass renders the
run's current state and the last one (all jobs done, or `final`)
closes the card through the same path as `watch`. Use it when
holding a runner for the whole run (watch: the full duration) is
the cost; use `watch` when one self-contained job is simpler and
the runner time does not matter. Concurrent refreshes are
last-writer-wins on an idempotent render.
· `report` single-job workflows, as the job's last step. One post at the
end; the steps are the rows. No second runner is paid for.
· `thread` post detail under an existing card, found by its metadata. A
Dagger call that used to open its own card posts here instead.

── IT NEVER WAITS FOR A HUMAN ──────────────────────────────────────────────
When all that is left of a run is jobs parked on an environment approval,
`watch` renders them as "awaiting approval", closes the card as `waiting` and
EXITS. A second watcher job that `needs` the gated job and passes
`resumeAfter` picks the same card up once the gate opens and carries it to
its verdict, so a released run does not stay "awaiting approval" forever. Polling an approval held a runner for up to thirty minutes and was
retired on 2026-08-22 (ADR-0003 §2.3); approvals are the `deploy-gate` service's
job, and it threads its request under this card by the commit sha in the
card's metadata.

── THE CARD'S METADATA IS API ──────────────────────────────────────────────
`event_type` = the caller's `eventType`; `event_payload` carries `repo`, `sha`,
`run_id`, `run_attempt` and `status`. `thread` finds cards by the first three
of (event_type, run_id, run_attempt); `deploy-gate` finds them by `sha`;
`slack.readTrend` reads `repo` and the numbers. See `slack`'s `EventPayload`.

── TEST REPORTS CROSS JOBS AS ARTIFACTS ────────────────────────────────────
A `wildbit.test-report/v1` built in job A has to reach the watcher in job B,
on another runner, with no shared disk. Each job uploads its report with
`actions/upload-artifact` under a name starting `test-report-` (with
`overwrite: true`), and `watch` reads them back through `github.runArtifactFiles`
when it closes, merges them with `testing.merge`, maps features when it has the
specs, and forwards ONE report to `slack.render`/`slack.breakdown`.

Chosen over a `runcard.attachReport` that posts each report into the card's
thread: that would put multi-kilobyte JSON through Slack metadata (capped, and
a report of 600 suites does not fit), race the watcher's first post, depend on
a Slack token every job would then need (Dependabot runs have none), and leave
nothing durable. An artifact needs only the `actions: read` the watcher already
holds, costs one step per job, is retained with the run, and is the file a
later Grafana import reads. `report` takes the reports directly: a single job
already has them in hand.

── NOTIFICATION IS BEST-EFFORT ─────────────────────────────────────────────
No function here fails a build because Slack or the API was slow. They throw
only on MISCONFIGURATION (bad JSON, a `selfJob` that matches nothing, a token
that cannot read the run) — the failures that would otherwise hold a runner
until the deadline and render a card that lies.

Installation

dagger install github.com/wildbitca/daggerverse/runcard@v0.4.0

Entrypoint

Return Type
Runcard
Example
dagger -m github.com/wildbitca/daggerverse/runcard@6edf4b951119acbf92be245ff8954794254f8e6d call \
func (m *MyModule) Example() *dagger.Runcard  {
	return dag.
			Runcard()
}
@function
def example() -> dagger.Runcard:
	return (
		dag.runcard()
	)
@func()
example(): Runcard {
	return dag
		.runcard()
}

Types

Runcard 🔗

watch() 🔗

Keep one card live for a multi-job run. Returns ok, fail, waiting, or skipped when there is no Slack token/channel (Dependabot, fork PRs).

Run it in a job with NO needs, continue-on-error: true, and a timeout-minutes above deadlineMinutes. It polls every pollSeconds, posts each failed job once to the thread (failing step, annotations, log tail), and closes with statistics. It exits — never waits — once only approval-gated jobs remain.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
repoString !-No description provided
refString !-No description provided
shaString !-No description provided
actorString !-No description provided
eventString !-No description provided
runIdString !-No description provided
runNumberString !-No description provided
runAttemptString !-No description provided
serverString !-No description provided
rowsString !-

JSON [{row, match, mode}] in card order. match is a job name (exact) or its prefix (prefix, the default, which aggregates a matrix as n/m). When two rows match one job the most specific wins.

selfJobString !-

the exact name: of the job running this watcher. Required: a watcher that counts itself never sees the run finish and holds its runner to the deadline, so a name that matches no job is an error.

eventTypeString !-

Slack metadata event_type, ^[a-z0-9_]+$, e.g. pipeline_app. Also the trend series readTrend compares against.

githubTokenSecret !-

needs actions: read and checks: read on repo.

slackChannelString !-No description provided
slackTokenSecret -

omit the flag when there is no token; an empty one is treated the same.

workflowFileString !""

file name for the median (pipeline.yml); empty skips it.

branchString !""

branch the median is taken on; empty means all branches.

msgString !""No description provided
titleString !""No description provided
runnerPricesString !""

JSON {label: usdPerMinute} merged over the defaults (ubuntu-latest 0.006, macos-* 0.062). * globs are allowed.

pollSecondsInteger !30No description provided
deadlineMinutesInteger !100No description provided
resumeAfterString !""

the exact name: of the watcher job that stopped at an approval. Set it on a second watcher job that needs the gated job (with if: always()): it edits that run’s existing card instead of posting a new one, and does not re-post failures the first watcher already threaded (every job that failed before that watcher completed).

reportArtifactsString !""

artifact name prefix, e.g. test-report-. At close the watcher reads every *.json inside the run’s artifacts with that prefix, keeps the wildbit.test-report/v1 ones, merges them and renders the card’s test line and the thread’s test sections from the result. Empty (default) reads nothing. Jobs upload with overwrite: true so a re-run replaces its report instead of failing the upload.

testReportsString !""

JSON of reports in hand (one or an array), merged with the artifacts. For a resuming watcher that needs a job exposing one as output.

specsDirectory -

the specs/features directory (sparse checkout in the watcher job). When given, the merged report is mapped to features and any drift is posted to the thread — report-only.

coverageMapFile -

a coverage map outside specs (overrides specs/coverage.tsv).

testReportsFileFile -

the same as testReports, from a file — a real report passes the ~128 kB argv limit, so a workflow passes the path, never the text.

Example
dagger -m github.com/wildbitca/daggerverse/runcard@6edf4b951119acbf92be245ff8954794254f8e6d call \
 watch --repo string --ref string --sha string --actor string --event string --run-id string --run-number string --run-attempt string --server string --rows string --self-job string --event-type string --github-token env:MYSECRET --slack-channel string --workflow-file string --branch string --msg string --title string --runner-prices string --poll-seconds integer --deadline-minutes integer --resume-after string --report-artifacts string --test-reports string
func (m *MyModule) Example(ctx context.Context, repo string, ref string, sha string, actor string, event string, runId string, runNumber string, runAttempt string, server string, rows string, selfJob string, eventType string, githubToken *dagger.Secret, slackChannel string, workflowFile string, branch string, msg string, title string, runnerPrices string, pollSeconds int, deadlineMinutes int, resumeAfter string, reportArtifacts string, testReports string) string  {
	return dag.
			Runcard().
			Watch(ctx, repo, ref, sha, actor, event, runId, runNumber, runAttempt, server, rows, selfJob, eventType, githubToken, slackChannel, workflowFile, branch, msg, title, runnerPrices, pollSeconds, deadlineMinutes, resumeAfter, reportArtifacts, testReports)
}
@function
async def example(repo: str, ref: str, sha: str, actor: str, event: str, runid: str, runnumber: str, runattempt: str, server: str, rows: str, selfjob: str, eventtype: str, githubtoken: dagger.Secret, slackchannel: str, workflowfile: str, branch: str, msg: str, title: str, runnerprices: str, pollseconds: int, deadlineminutes: int, resumeafter: str, reportartifacts: str, testreports: str) -> str:
	return await (
		dag.runcard()
		.watch(repo, ref, sha, actor, event, runid, runnumber, runattempt, server, rows, selfjob, eventtype, githubtoken, slackchannel, workflowfile, branch, msg, title, runnerprices, pollseconds, deadlineminutes, resumeafter, reportartifacts, testreports)
	)
@func()
async example(repo: string, ref: string, sha: string, actor: string, event: string, runId: string, runNumber: string, runAttempt: string, server: string, rows: string, selfJob: string, eventType: string, githubToken: Secret, slackChannel: string, workflowFile: string, branch: string, msg: string, title: string, runnerPrices: string, pollSeconds: number, deadlineMinutes: number, resumeAfter: string, reportArtifacts: string, testReports: string): Promise<string> {
	return dag
		.runcard()
		.watch(repo, ref, sha, actor, event, runId, runNumber, runAttempt, server, rows, selfJob, eventType, githubToken, slackChannel, workflowFile, branch, msg, title, runnerPrices, pollSeconds, deadlineMinutes, resumeAfter, reportArtifacts, testReports)
}

refresh() 🔗

ONE pass that brings the run’s card up to date, then returns. Event-driven alternative to watch: call it from a job that runs when something changes (a job finishing, via needs + if: always()), so no runner is held polling. Returns posted, updated, closed:<ok|fail>, or skipped when there is no Slack token/channel.

It looks the card up by (eventType, runId, runAttempt) and posts a new one only when none exists, reads the run’s jobs once, renders the same rows as watch, and either updates the card in place (status running) or closes it exactly as watch does: final status, test reports merged from artifacts, feature map, breakdown and statistics in the thread. It closes when every job except selfJob has completed and each row has a job, or when final is true. Jobs parked on an approval render “awaiting approval” and the card shows waiting; that is not a close, so nothing is threaded and a later refresh carries it on once the gate opens.

Concurrency: refreshers that run close together each render from the API’s current state, so updates are idempotent and the last writer wins; a stale write is corrected by the next refresh. Two refreshers can still both post a NEW card if neither finds one yet (the first refresh of a run); the lookup before posting makes that rare, not impossible. Have the first job call it early, or accept a rare duplicate.

Differences from watch: failed jobs are threaded once, at close (a one-shot has no memory of what it already posted); a final refresh with jobs still open marks them failed (“watcher deadline”).

Return Type
String !
Arguments
NameTypeDefault ValueDescription
repoString !-No description provided
refString !-No description provided
shaString !-No description provided
actorString !-No description provided
eventString !-No description provided
runIdString !-No description provided
runNumberString !-No description provided
runAttemptString !-No description provided
serverString !-No description provided
rowsString !-

, eventType, workflowFile, branch, runnerPrices, reportArtifacts, testReports, testReportsFile, specs, coverageMap as in watch.

selfJobString !-

the exact name: of the calling job; excluded from the rows and the completion check. A name that matches no job is an error.

eventTypeString !-No description provided
githubTokenSecret !-No description provided
slackChannelString !-No description provided
slackTokenSecret -No description provided
workflowFileString !""No description provided
branchString !""No description provided
msgString !""No description provided
titleString !""No description provided
runnerPricesString !""No description provided
reportArtifactsString !""No description provided
testReportsString !""No description provided
specsDirectory -No description provided
coverageMapFile -No description provided
testReportsFileFile -No description provided
finalBoolean !false

force the close even when a row has no job (an if: that skipped it); set it on the last job of the workflow.

Example
dagger -m github.com/wildbitca/daggerverse/runcard@6edf4b951119acbf92be245ff8954794254f8e6d call \
 refresh --repo string --ref string --sha string --actor string --event string --run-id string --run-number string --run-attempt string --server string --rows string --self-job string --event-type string --github-token env:MYSECRET --slack-channel string --workflow-file string --branch string --msg string --title string --runner-prices string --report-artifacts string --test-reports string --final boolean
func (m *MyModule) Example(ctx context.Context, repo string, ref string, sha string, actor string, event string, runId string, runNumber string, runAttempt string, server string, rows string, selfJob string, eventType string, githubToken *dagger.Secret, slackChannel string, workflowFile string, branch string, msg string, title string, runnerPrices string, reportArtifacts string, testReports string, final bool) string  {
	return dag.
			Runcard().
			Refresh(ctx, repo, ref, sha, actor, event, runId, runNumber, runAttempt, server, rows, selfJob, eventType, githubToken, slackChannel, workflowFile, branch, msg, title, runnerPrices, reportArtifacts, testReports, final)
}
@function
async def example(repo: str, ref: str, sha: str, actor: str, event: str, runid: str, runnumber: str, runattempt: str, server: str, rows: str, selfjob: str, eventtype: str, githubtoken: dagger.Secret, slackchannel: str, workflowfile: str, branch: str, msg: str, title: str, runnerprices: str, reportartifacts: str, testreports: str, final: bool) -> str:
	return await (
		dag.runcard()
		.refresh(repo, ref, sha, actor, event, runid, runnumber, runattempt, server, rows, selfjob, eventtype, githubtoken, slackchannel, workflowfile, branch, msg, title, runnerprices, reportartifacts, testreports, final)
	)
@func()
async example(repo: string, ref: string, sha: string, actor: string, event: string, runId: string, runNumber: string, runAttempt: string, server: string, rows: string, selfJob: string, eventType: string, githubToken: Secret, slackChannel: string, workflowFile: string, branch: string, msg: string, title: string, runnerPrices: string, reportArtifacts: string, testReports: string, final: boolean): Promise<string> {
	return dag
		.runcard()
		.refresh(repo, ref, sha, actor, event, runId, runNumber, runAttempt, server, rows, selfJob, eventType, githubToken, slackChannel, workflowFile, branch, msg, title, runnerPrices, reportArtifacts, testReports, final)
}

refreshPlan() 🔗

The decision refresh makes, exposed pure so it can be tested on fixtures. Returns JSON {items, outcome, status, close} (outcome is running|approval|done|deadline).

Return Type
String !
Arguments
NameTypeDefault ValueDescription
jobsString !-

JSON GhJob[] — github.runJobs output or a fixture.

rowsString !-

JSON [{row, match, mode}].

selfJobString !""

the calling job’s name, excluded as in refresh.

finalBoolean !false

the final flag of refresh.

Example
dagger -m github.com/wildbitca/daggerverse/runcard@6edf4b951119acbf92be245ff8954794254f8e6d call \
 refresh-plan --jobs string --rows string --self-job string --final boolean
func (m *MyModule) Example(ctx context.Context, jobs string, rows string, selfJob string, final bool) string  {
	return dag.
			Runcard().
			Refreshplan(ctx, jobs, rows, selfJob, final)
}
@function
async def example(jobs: str, rows: str, selfjob: str, final: bool) -> str:
	return await (
		dag.runcard()
		.refreshplan(jobs, rows, selfjob, final)
	)
@func()
async example(jobs: string, rows: string, selfJob: string, final: boolean): Promise<string> {
	return dag
		.runcard()
		.refreshPlan(jobs, rows, selfJob, final)
}

report() 🔗

One final card for a SINGLE-JOB workflow, from that job’s last step. Returns ok, fail or skipped.

Call it with if: always() and --job-status=${{ job.status }}. The rows are the job’s steps as the API reports them at that moment: completed steps only, without the runner’s own Set up job / Post … / Complete job. The log is not published until the job completes, so a failure’s thread carries the failing step and annotations plus a link, not a log tail.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
repoString !-No description provided
refString !-No description provided
shaString !-No description provided
actorString !-No description provided
eventString !-No description provided
runIdString !-No description provided
runNumberString !-No description provided
runAttemptString !-No description provided
serverString !-No description provided
jobStatusString !-

${{ job.status }}: success | failure | cancelled.

eventTypeString !-No description provided
githubTokenSecret !-No description provided
slackChannelString !-No description provided
slackTokenSecret -No description provided
jobNameString !""

the job’s name:; empty works when the run has exactly one job.

rowsString !""

optional JSON [{row, match, mode}] matched against STEP names, to group or rename steps. Empty means one row per step.

msgString !""No description provided
titleString !""No description provided
testReportsString !""

JSON of this job’s wildbit.test-report/v1 reports (one or an array). They are merged, drive the card’s test line, and add a breakdown to the thread. Empty keeps the card exactly as before.

testReportsFileFile -

the same, from a file (--test-reports-file=./test-report.json); a report of hundreds of suites is too long for a command-line argument.

specsDirectory -

specs/features, to map the merged report to features (report-only).

coverageMapFile -

a coverage map outside specs.

Example
dagger -m github.com/wildbitca/daggerverse/runcard@6edf4b951119acbf92be245ff8954794254f8e6d call \
 report --repo string --ref string --sha string --actor string --event string --run-id string --run-number string --run-attempt string --server string --job-status string --event-type string --github-token env:MYSECRET --slack-channel string --job-name string --rows string --msg string --title string --test-reports string
func (m *MyModule) Example(ctx context.Context, repo string, ref string, sha string, actor string, event string, runId string, runNumber string, runAttempt string, server string, jobStatus string, eventType string, githubToken *dagger.Secret, slackChannel string, jobName string, rows string, msg string, title string, testReports string) string  {
	return dag.
			Runcard().
			Report(ctx, repo, ref, sha, actor, event, runId, runNumber, runAttempt, server, jobStatus, eventType, githubToken, slackChannel, jobName, rows, msg, title, testReports)
}
@function
async def example(repo: str, ref: str, sha: str, actor: str, event: str, runid: str, runnumber: str, runattempt: str, server: str, jobstatus: str, eventtype: str, githubtoken: dagger.Secret, slackchannel: str, jobname: str, rows: str, msg: str, title: str, testreports: str) -> str:
	return await (
		dag.runcard()
		.report(repo, ref, sha, actor, event, runid, runnumber, runattempt, server, jobstatus, eventtype, githubtoken, slackchannel, jobname, rows, msg, title, testreports)
	)
@func()
async example(repo: string, ref: string, sha: string, actor: string, event: string, runId: string, runNumber: string, runAttempt: string, server: string, jobStatus: string, eventType: string, githubToken: Secret, slackChannel: string, jobName: string, rows: string, msg: string, title: string, testReports: string): Promise<string> {
	return dag
		.runcard()
		.report(repo, ref, sha, actor, event, runId, runNumber, runAttempt, server, jobStatus, eventType, githubToken, slackChannel, jobName, rows, msg, title, testReports)
}

thread() 🔗

Reply in the thread of a run’s card. Returns the reply’s ts, or “” when no card was found in time (best-effort: never throws on Slack errors).

The card is found by metadata — eventType + runId (+ runAttempt) — so a Dagger call in any job of the run can post its inner detail (a test breakdown, a failing flow) under the watcher’s card instead of opening a second one. It retries for waitSeconds, because the job calling it may start before the watcher has posted.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
slackChannelString !-

the channel ID (C…); history lookups reject names.

eventTypeString !-No description provided
runIdString !-No description provided
textString !-No description provided
slackTokenSecret -No description provided
runAttemptString !""

empty matches the newest card of any attempt.

blocksString !""

optional JSON Block Kit array; text is then the notification fallback.

waitSecondsInteger !60No description provided
Example
dagger -m github.com/wildbitca/daggerverse/runcard@6edf4b951119acbf92be245ff8954794254f8e6d call \
 thread --slack-channel string --event-type string --run-id string --text string --run-attempt string --blocks string --wait-seconds integer
func (m *MyModule) Example(ctx context.Context, slackChannel string, eventType string, runId string, text string, runAttempt string, blocks string, waitSeconds int) string  {
	return dag.
			Runcard().
			Thread(ctx, slackChannel, eventType, runId, text, runAttempt, blocks, waitSeconds)
}
@function
async def example(slackchannel: str, eventtype: str, runid: str, text: str, runattempt: str, blocks: str, waitseconds: int) -> str:
	return await (
		dag.runcard()
		.thread(slackchannel, eventtype, runid, text, runattempt, blocks, waitseconds)
	)
@func()
async example(slackChannel: string, eventType: string, runId: string, text: string, runAttempt: string, blocks: string, waitSeconds: number): Promise<string> {
	return dag
		.runcard()
		.thread(slackChannel, eventType, runId, text, runAttempt, blocks, waitSeconds)
}

cardTs() 🔗

The ts of a run’s card, or “”. For callers that want slack.breakdown or slack.failureDetail under the card rather than a plain reply.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
slackChannelString !-No description provided
eventTypeString !-No description provided
runIdString !-No description provided
slackTokenSecret -No description provided
runAttemptString !""No description provided
waitSecondsInteger !60No description provided
Example
dagger -m github.com/wildbitca/daggerverse/runcard@6edf4b951119acbf92be245ff8954794254f8e6d call \
 card-ts --slack-channel string --event-type string --run-id string --run-attempt string --wait-seconds integer
func (m *MyModule) Example(ctx context.Context, slackChannel string, eventType string, runId string, runAttempt string, waitSeconds int) string  {
	return dag.
			Runcard().
			Cardts(ctx, slackChannel, eventType, runId, runAttempt, waitSeconds)
}
@function
async def example(slackchannel: str, eventtype: str, runid: str, runattempt: str, waitseconds: int) -> str:
	return await (
		dag.runcard()
		.cardts(slackchannel, eventtype, runid, runattempt, waitseconds)
	)
@func()
async example(slackChannel: string, eventType: string, runId: string, runAttempt: string, waitSeconds: number): Promise<string> {
	return dag
		.runcard()
		.cardTs(slackChannel, eventType, runId, runAttempt, waitSeconds)
}

cardRows() 🔗

The row logic, exposed pure so it can be tested on fixtures. Returns JSON Item[].

Return Type
String !
Arguments
NameTypeDefault ValueDescription
jobsString !-

JSON GhJob[] — github.runJobs output or a fixture.

rowsString !-

JSON [{row, match, mode}].

selfJobString !""

a job name to exclude, as watch does.

Example
dagger -m github.com/wildbitca/daggerverse/runcard@6edf4b951119acbf92be245ff8954794254f8e6d call \
 card-rows --jobs string --rows string --self-job string
func (m *MyModule) Example(ctx context.Context, jobs string, rows string, selfJob string) string  {
	return dag.
			Runcard().
			Cardrows(ctx, jobs, rows, selfJob)
}
@function
async def example(jobs: str, rows: str, selfjob: str) -> str:
	return await (
		dag.runcard()
		.cardrows(jobs, rows, selfjob)
	)
@func()
async example(jobs: string, rows: string, selfJob: string): Promise<string> {
	return dag
		.runcard()
		.cardRows(jobs, rows, selfJob)
}

runReports() 🔗

The run’s test reports as ONE merged wildbit.test-report/v1, plus notes for the thread (unreadable artifacts, feature-map drift). Returns JSON {report, notes}; report is the report’s JSON string, “” when there is none.

What watch and report do at close, exposed so a job can reuse it — e.g. to write testing.summaryMarkdown of the whole run to $GITHUB_STEP_SUMMARY — and so the artifact handoff is probed in CI without Slack.

Best-effort: a report that cannot be read is a note, never an error — the card’s verdict comes from the jobs, and the reports only describe it. Only a malformed testReports argument throws, because that is the caller’s wiring.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
githubTokenSecret !-No description provided
repoString !-No description provided
runIdString !-No description provided
cacheBustString !-

MUST vary per call; artifacts uploaded since are otherwise invisible.

reportArtifactsString !""

artifact name prefix (test-report-); empty reads none.

testReportsString !""

JSON reports in hand, one or an array.

specsDirectory -No description provided
coverageMapFile -No description provided
testReportsFileFile -No description provided
Example
dagger -m github.com/wildbitca/daggerverse/runcard@6edf4b951119acbf92be245ff8954794254f8e6d call \
 run-reports --github-token env:MYSECRET --repo string --run-id string --cache-bust string --report-artifacts string --test-reports string
func (m *MyModule) Example(ctx context.Context, githubToken *dagger.Secret, repo string, runId string, cacheBust string, reportArtifacts string, testReports string) string  {
	return dag.
			Runcard().
			Runreports(ctx, githubToken, repo, runId, cacheBust, reportArtifacts, testReports)
}
@function
async def example(githubtoken: dagger.Secret, repo: str, runid: str, cachebust: str, reportartifacts: str, testreports: str) -> str:
	return await (
		dag.runcard()
		.runreports(githubtoken, repo, runid, cachebust, reportartifacts, testreports)
	)
@func()
async example(githubToken: Secret, repo: string, runId: string, cacheBust: string, reportArtifacts: string, testReports: string): Promise<string> {
	return dag
		.runcard()
		.runReports(githubToken, repo, runId, cacheBust, reportArtifacts, testReports)
}