Skip to content

refactor: test reporting and notifications into separate commands - #223

Open
metalwarrior665 wants to merge 14 commits into
masterfrom
claude/kind-lamport-dyov8p
Open

metalwarrior665 wants to merge 14 commits into
masterfrom
claude/kind-lamport-dyov8p

Conversation

@metalwarrior665

@metalwarrior665 metalwarrior665 commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Check #notif-testing-repo-for-github-actions channel to see live test of this branch.

AI Summary

This PR refactors the test reporting and notification system by splitting the monolithic report-tests command into two focused commands: test (which runs vitest and produces a report) and notify (which sends notifications for both test results and releases).

Key Changes

  • New test command (bin/run-tests.ts): Runs the repository's vitest suite on specified builds or all Actors, producing a structured TestReport that replaces the previous JSON parsing approach. Handles vitest binary resolution from the repository's node_modules and manages parallelism settings.

  • New notify command (bin/notify.ts): Unified notification system that handles both test reports and release announcements. Replaces the separate report-tests and notify-release commands with a single entry point that reads different input types via stdin.

  • Refactored test-report.ts: Converted from a reporting script into a data structure and conversion module. Defines TestReport, TestCaseReport, and TestRun interfaces that serve as the contract between test and notify. Includes toTestReport() to convert vitest's JSON output to the standardized format.

  • Updated stdin-input.ts: Added parsers for test builds (parseTestBuilds) and unified notify input (parseNotifyInput), replacing the previous parseReleaseChanges. Validates that flags match the input type (e.g., --changelog-only only works with releases).

  • Simplified slack.ts: Removed release-specific notification logic; now only provides low-level Slack client utilities (getSlackClient, sendSlackMessage, sendingLabel).

  • Updated CLI entry point (bin/main.ts): Registered new test and notify commands with their respective options. Removed report-tests and notify-release commands.

  • Updated workflows: Modified GitHub Actions workflows to use the new test and notify commands instead of the old report-tests and notify-release.

Notable Implementation Details

  • The test command resolves vitest from the repository's node_modules to ensure tests use the locked version, not the bundled CLI's version.
  • notify creates notification targets (currently Slack only) only when a message may be sent, so broken tokens are caught early but unnecessary token checks are avoided.
  • The TestReport format is versioned (v1) to allow future evolution while maintaining backward compatibility.
  • Test failures are reported as a structured report rather than directly posting to Slack, enabling multiple notification targets and better testability.
  • Input validation ensures flags are only used with compatible input types, preventing silent misconfigurations.

https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm

`test` wraps the repository's own vitest: it reads the builds from `build` on
stdin (or tests every default build with `--all`), sets ACTOR_BUILDS /
RUN_ALL_PLATFORM_TESTS and the concurrency flags itself, sends vitest's output
to stderr, logs every test with its run link, and prints a versioned JSON
report on stdout. Test files that fail to load and vitest crashing without a
report now count as failures; `report-tests` silently ignored them.

`notify` reads that report and posts it to any number of Slack channels
(`--slack-channel`, repeatable), on failure or `--on always`, with the title
and job link defaulting to the GitHub Actions run. Targets are pluggable so
email can be added next to Slack. Thread replies are split to stay within
Slack's block limits, and test text is escaped.

The platform-tests workflow now runs `test` and `notify` as separate steps, so
the Slack token no longer reaches the repository's test code. The PR workflow
pipes the builds into `test` through a file instead of a step output.

BREAKING CHANGE: `report-tests` is removed; use `test | notify`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm
`notify` now takes either the report of `test` or the output of
`get-affected-actors` on stdin, told apart by their `tests` and `commits`
fields. For a release, `--slack-channel` gets the commits and changed files
(what `--report-slack-channel` did) and the new `--slack-changelog-channel`
gets the changelog additions (what `--release-slack-channel` did); both can be
repeated. Flags that only fit the other kind of input are an error instead of
being ignored. Tokens stay per kind: SLACK_TOKEN_TESTS_BOT for test runs,
SLACK_TOKEN_RELEASES_BOT for releases.

The release announcement moves from slack.ts to notify.ts and goes through
the same Slack target as test notifications; its messages are unchanged.

BREAKING CHANGE: `notify-release` is removed. Use `notify` with
`--slack-changelog-channel` in place of `--release-slack-channel` and
`--slack-channel` in place of `--report-slack-channel`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm
`test` always runs vitest with --maxConcurrency=20 --fileParallelism
--maxWorkers=100, the values every workflow used. The --max-concurrency and
--max-workers flags go away; a one-off override still works after `--`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm
Vitest rejects an option given twice, so `test -- --maxConcurrency 5` failed
with "Expected a single value". The hardcoded --maxConcurrency,
--fileParallelism and --maxWorkers now step aside when the caller passes the
same option (or its --no- form) after `--`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm
`notify` posts to one place per call; posting to more is calling it again.
`--slack-channel` takes a single channel (a repeated flag is an error rather
than a comma-joined channel), and `--slack-changelog-channel` is replaced by
`--changelog-only`, which picks what a release message says instead of where
it goes. The release workflow calls notify twice: the changelog to
#delivery-public-actors, the full report to the repo's channel.

The full release report now always includes the changelog additions; before,
it did only when a changelog channel was also passed.

BREAKING CHANGE: `--slack-channel` no longer repeats and
`--slack-changelog-channel` is removed; call `notify --changelog-only
--slack-channel <channel>` for the changelog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm
Master split the README into docs/cli.md, docs/lib.md and docs/workflows.md.
This branch's README changes move there: the `test` and `notify` sections
replace `report-tests` and `notify-release` in docs/cli.md, the local-run
examples pipe builds into `test`, and docs/workflows.md describes the workflows'
`test` and `notify` steps and where the Slack tokens reach. The CLI bundle is
rebuilt from the merged sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm
@metalwarrior665 metalwarrior665 changed the title Refactor test reporting and notifications into separate commands refactor: test reporting and notifications into separate commands Oct 29, 2026
Conflicts:
- bin/main.ts: keeps the `test` and `notify` options next to master's
  --require-env-vars-in-sync; `notify-release` and its options stay removed.
- public_pr-build-test.yaml: the build now passes --require-env-vars-in-sync
  and still writes builds.json for `test`.
- public_platform-tests.yaml: master's TESTER_APIFY_TOKEN check runs before
  the `test` step.
- The CLI bundle is rebuilt from the merged sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm
On GitHub Actions the runner sets GITHUB_SERVER_URL, GITHUB_REPOSITORY and
GITHUB_RUN_ID, so notify appends the run link and "posts passing runs too with
on: always" failed in CI while passing locally. Every test now starts without
them; the one that checks the defaults sets them itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm
- Test reports and releases both post with SLACK_BOT_TOKEN, replacing
  SLACK_TOKEN_TESTS_BOT and SLACK_TOKEN_RELEASES_BOT in the CLI, the
  workflows' secret declarations and the docs.
- `--changelog-only` is removed, and the release workflow posts once, the full
  message to the report channel; nothing goes to #delivery-public-actors.
- The release message drops the "Additions to the changelog" header, the
  "Commit message" prefix and the changed files. Each commit is its title, cut
  at 200 characters, linked to its GitHub commit page (plain text on other
  hosts), followed by its author. A rebase merge lists every commit.
- `notify` no longer reads `changedFiles` from the get-affected-actors output.

BREAKING CHANGE: callers pass SLACK_BOT_TOKEN instead of SLACK_TOKEN_TESTS_BOT
and SLACK_TOKEN_RELEASES_BOT, and `notify --changelog-only` is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm
…om the CLI

- `test` no longer passes arguments after `--` to vitest. Nothing used it, and
  it let a caller replace the reporters the report depends on. The
  parallelism flags are fixed again, so the duplicate-option handling goes too.
  Running vitest directly with ACTOR_BUILDS still covers one-off options.
- `notify` no longer reads GITHUB_* variables for its title and job link: they
  are plain `--title` and `--job-url` parameters, as `report-tests` had them,
  and the platform-tests workflow passes the workflow name and the run URL.
  The CLI stays CI-agnostic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm
…test

Without the token the lib skips every platform test, so `test` passed having
tested nothing, with only a warning. It now fails before starting vitest. The
workflows keep their own early check, which in the PR workflow also stops the
job before any Actor is built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm
Docs and comments state how things work now; how they used to work belongs in
PR descriptions. Rewrites the `test --all` paragraph, the change-detection note
on `code/` and `shared/`, the git range commands section, the workflows'
`report-slack-channel` comments, the release-guard note in CONTRIBUTING.md, the
`devel` tag comment in build.ts and one test name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm
… a quiet Notify step

- `test --reporter <name>` adds vitest reporters next to the default and JSON
  ones. Both workflows pass `github-actions`, so failing tests are annotated
  on the run again; passing reporters replaced vitest's own default, which adds
  it on GitHub Actions.
- The Slack failure message is the one report-tests posted: "has N failed
  assertions. Failing test suites: F/T.", one line per failure message as
  `message --- <runLink|actorId>`, the title defaulting to `-`. The report
  keeps every failure message again, so the counts match. Text from the tests
  is escaped, a failure without a run gets no link, and errors outside of
  tests are counted and listed.
- The scheduled workflow's Notify step runs only when the Test step ran, and
  skips an empty report, so a failed token check no longer adds a second red
  step with a confusing error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm
# secrets:
# TESTER_APIFY_TOKEN: ${{ secrets.TESTER_APIFY_TOKEN }}
# SLACK_TOKEN_TESTS_BOT: ${{ secrets.SLACK_TOKEN_TESTS_BOT }}
# SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Merged both Slack tokens to this one. I already added SLACK_BOT_TOKEN to our orgs

SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
run: |
apify-test-tools notify \
--slack-channel "${{ inputs.report-slack-channel || format('#notif-{0}', github.event.repository.name) }}" \

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Keeping the channel fallback now before teams add the workflow inputs. We remove the fallback before releasing this to community.

Comment thread bin/notify.ts Outdated
@@ -0,0 +1,221 @@
import process from 'node:process';

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This file should be 1:1 with current code, just with few small UX improvements

Comment thread bin/notify.ts
const SLACK_TOKEN_ENV_VAR = 'SLACK_BOT_TOKEN';

// Long enough for any sensible commit title, short enough that one runaway title doesn't swamp the message
const MAX_COMMIT_TITLE_LENGTH = 200;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Commit messages could spam a page of text before

Comment thread bin/notify.ts
? `${message.slice(0, MAX_COMMIT_TITLE_LENGTH - 1)}…`
: message;
const url = getCommitUrl(repoUrl, sha);
const linked = url ? `<${url}|${escapeSlack(title)}>` : escapeSlack(title);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Commits now contain links to them

SLACK_TOKEN_RELEASES_BOT: ${{ secrets.SLACK_TOKEN_RELEASES_BOT }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
run: |
apify-test-tools notify-release --release-slack-channel "#delivery-public-actors" --report-slack-channel "${{ inputs.report-slack-channel || format('#notif-{0}', github.event.repository.name) }}" < affected.json

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No more 2 channels, #delivery-public-actors is dead

metalwarrior665 added a commit to apify-store/testing-repo-for-github-actions that referenced this pull request Oct 30, 2026
…nch (#62)

* test: run the workflows from apify-test-tools' test and notify branch

Points every workflow at apify/apify-test-tools@claude/kind-lamport-dyov8p
(apify/apify-test-tools#223) and touches src/main.ts so the PR workflow builds
and tests Actors 1 and 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm

* test: make both core tests fail on purpose

To see how failing tests are reported: as annotations on this PR, and on Slack
once the scheduled core tests run them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm

* test: skip the env var sync check for this PR's builds

dummy-for-testing-1's version 0.99 has env vars out of sync with 0.0 on
purpose, which fails the build before the tests this PR is about.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm

---------

Co-authored-by: Claude <noreply@anthropic.com>
Vitest's JSON reporter logs "JSON report written to <path>" whenever it
writes its report to a file, which for `test` is a temporary file nobody
needs. Vitest's stdout is now piped through `test` and passed on to stderr
without that line. Colors stay on when stderr is a terminal, since vitest
no longer sees one on its own stdout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149HKZkpP35zMLDjVKKf6Cm
Comment thread bin/main.ts
'release-slack-channel': { type: 'string', describe: 'Channel for the changelog additions only' },
/** Flags of `test`, which runs the repository's vitest on the builds piped in from `build`. */
const testOptions = {
all: {

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.

bad name, on the workflow I thought it meant test all actors (which made no sense since you passed a blob) so i went looking for this definition.
Why not --latest or --default-build

Comment thread bin/main.ts
'vitest-output-file': { type: 'string', describe: "Also keep vitest's own JSON report there, e.g. to upload it" },
reporter: {
type: 'string',
describe: 'Vitest reporter to add to the default one, e.g. github-actions. Repeat it for more',

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.

docs link would be useful here.
Also mention the ones set by default (default and json)

Comment thread bin/main.ts
.option('report-slack-channel', { type: 'string' })
.option('job-url', { type: 'string' })
.option('workflow-name', { type: 'string' }),
.positional('filters', {

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.

being this positional and defined first, does it even collide with --reporter and its fake array?

Comment thread bin/notify.ts
/** A flag the input has no use for would be dropped silently, so it fails instead: the caller meant something by it. */
const assertFlagsFitInput = ({ input, on, title, jobUrl, repoUrl }: NotifyOptions) => {
const misfits =
input.kind === 'tests'

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.

nit: ternary for "enum" means we'll have to restructure the whole thing if we ever get a different use case (like test summaries).
Did a snippet of how it could look on a modification-safe way:

const misfits: string[] = [];
let stdinInputDescriptor: string;
switch (input.kind) {
    case 'tests':
        stdinInputDescriptor = 'a test report (the output of test)';
        if (changelogOnly) misfits.push('--changelog-only');
        if (repoUrl) misfits.push('--repo-url');
        break;
    case 'release':
        stdinInputDescriptor = 'a release (the output of get-affected-actors)';
        if (on !== 'failure') misfits.push('--on');
        if (title) misfits.push('--title');
        if (jobUrl) misfits.push('--job-url');
        break;
    default:
        throw unreachable(input);
}
if (misfits.length > 0) {
    throw new Error(`${misfits.join(', ')} can't be used with ${stdinInputDescriptor} on stdin`);
}

Probably overkill tho

Comment thread bin/notify.ts

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.

did a nit comment about input.kind and its possible modification on assertFlagsFitInput.
Looking at the file as a whole, this seems like a place for strategy pattern (instead of that switch) since we're bound to have split behavior for all different types of it

Comment thread bin/stdin-input.ts

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.

branding our outputs (as in with a kind prop) would make our lives easier. (not to be solved here)

Comment thread bin/stdin-input.ts
'e.g. apify-test-tools test --all | apify-test-tools notify --slack-channel "#my-channel", ' +
'or apify-test-tools get-affected-actors --since <sha> | apify-test-tools notify --slack-channel "#my-channel"';
const expected = 'the output of test or get-affected-actors';
const json = parseStdinJson(raw, z.unknown(), { expected, example });

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.

Checking for it being an object at least helps with the has. An unknown is pretty much useless

Suggested change
const json = parseStdinJson(raw, z.unknown(), { expected, example });
const json = parseStdinJson(raw, z.looseObject({}), { expected, example });

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.

3 participants