Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions EXTERNAL-VOICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# External voice: no em-dashes

This file is the canonical rule. It is copied verbatim into every repo that owns
a public surface: `agents`, `web-client`, `customer-agents`,
`test-reporter-action`. Change it in one place and port the change to the rest.

## Scope

Everything BuildPulse publishes or sends outside the company:

- Blog posts and the prompts that generate them
- Marketing pages, pricing, comparison, and about copy
- Social captions, video scripts, voiceover, and audio explainers
- Public READMEs and docs
- Review comments and notices we post on customer pull requests
- In-product copy a customer reads
- Outbound email

Out of scope: internal code comments, internal runbooks, log lines, commit
messages, and anything only employees read. Those are not worth the diff noise.

## The rule

Do not use the em-dash (`—`, U+2014) or the horizontal bar (`―`, U+2015).

Do not use an en-dash (U+2013) with a space on either side in their place. That
is the same tell wearing a hat.

Keep the unspaced en-dash in numeric ranges: `20–40%`, a `100–800` person
company. That is correct typography and reads as human.

Never substitute a hyphen with a space on either side. It is a worse em-dash and
it reads as a find-and-replace job rather than a rewrite.

(Those two rules are written out in words on purpose. Written as inline code the
significant spaces get trimmed by a formatter, which silently inverts what the
rule says.)

## How to rewrite, by job

An em-dash does at least five different jobs. Each has its own correct fix.
Pick by what the dash was actually doing, not by what is quickest.

| Job | Use instead | Before | After |
| ------------------------------------ | ------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Abrupt pivot into a punchline | Full stop, two sentences | `You didn't fix it — you bribed it.` | `You didn't fix it. You bribed it.` |
| Introducing an explanation | Colon | `Code blocks don't count — a code-heavy post needs the same prose.` | `Code blocks don't count: a code-heavy post needs the same prose.` |
| Label before a definition, in a list | Colon | `**Bugs** — logic errors, nil deref.` | `**Bugs**: logic errors, nil deref.` |
| Paired aside | Parentheses, or commas if short | `environments — fintech, healthtech — where CI gates` | `environments (fintech, healthtech) where CI gates` |
| Clause tacked on the end | Comma, or recast | `...in AI features — or email support.` | `...in AI features, or email support.` |

If none of those land cleanly, rewrite the sentence. The dash was often hiding a
sentence that wanted to be two.

## Why

Two reasons, and the second is the one that matters.

The em-dash is the single most recognizable tell of machine-written prose, and
our audience is engineers who spot it instantly. A post that reads as generated
undercuts the argument in it, and a review comment that reads as generated gets
dismissed before the finding is read.

More importantly, the dash is a way to avoid deciding what the relationship
between two clauses actually is. Choosing a colon, a full stop, or parentheses
forces that decision, and the prose gets sharper for it.

## For prompts specifically

A prompt that bans em-dashes while being written in em-dash style will lose to
its own example. Models copy the register of their instructions more reliably
than they follow a rule buried in a bullet list.

So: strip the dashes from the prompt's own prose, then state the rule. Both, not
either.

State the rule as a taxonomy, not a prohibition. Told only "no em-dashes", a
model swaps in a spaced hyphen and you have gained nothing.

## Enforcement

| Surface | Guard |
| ---------------------------- | ------------------------------------------------------------------------------------- |
| Blog posts | `agents/blog-generator/validate.go` hard reject, fed back into the model's retry loop |
| Social captions | `agents/social-poster/compose.go` re-asks once, then publishes and warns |
| Video captions and voiceover | `agents/video-poster/shotlist.go` critique pass checks for it |
| Customer PR cap notice | `customer-agents/review-trigger/capnotice_test.go` |
| Marketing pages | `web-client/__tests__/marketing/emDashGuard.test.ts` |

Anything not in that table is on the author. The guards exempt this file, since
it necessarily quotes the character it bans, and they exempt fenced code blocks,
because a dash inside sample output belongs to the code and not to our voice.
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ Create an API token in your BuildPulse organization settings and store it as a r

## What's New in v3

- **Secret masking** — `api-token`, `key`, and `secret` are masked in CI logs via `core.setSecret()` so they never leak even if a downstream tool prints the request body.
- **Streaming upload to S3** — large monorepo archives are streamed instead of buffered in memory; multi-GB uploads no longer pressure the runner.
- **Retries with exponential backoff** — transient 5xx and network errors retry automatically instead of failing the customer build on a single blip.
- **HTTPS enforced** — the action now refuses to send credentials to a non-HTTPS `api-host`.
- **Real test suite** — `npm test` exercises auth, archive, upload (with mocked S3), and metadata across GitHub Actions / Bitbucket Pipelines environments.
- **Removed unused vulnerable deps** — `fast-xml-parser` (critical CVE) and other unused packages dropped.
- **Secret masking**: `api-token`, `key`, and `secret` are masked in CI logs via `core.setSecret()` so they never leak even if a downstream tool prints the request body.
- **Streaming upload to S3**: large monorepo archives are streamed instead of buffered in memory; multi-GB uploads no longer pressure the runner.
- **Retries with exponential backoff**: transient 5xx and network errors retry automatically instead of failing the customer build on a single blip.
- **HTTPS enforced**: the action now refuses to send credentials to a non-HTTPS `api-host`.
- **Real test suite**: `npm test` exercises auth, archive, upload (with mocked S3), and metadata across GitHub Actions / Bitbucket Pipelines environments.
- **Removed unused vulnerable deps**: `fast-xml-parser` (critical CVE) and other unused packages dropped.

## Upgrading From v2 (`buildpulse/buildpulse-action`)

Expand Down Expand Up @@ -104,7 +104,7 @@ New projects should use `api-token` instead.
| Input | Required | Description |
|-------|----------|-------------|
| `api-token` | Recommended | BuildPulse API token from organization settings |
| `path` | Yes | Path to JUnit XML file(s) — file, directory, or glob |
| `path` | Yes | Path to JUnit XML file(s): file, directory, or glob |
| `account` | Legacy only | BuildPulse account ID |
| `repository` | Legacy only | BuildPulse repository ID |
| `key` | Legacy only | `BUILDPULSE_ACCESS_KEY_ID` |
Expand Down Expand Up @@ -138,7 +138,7 @@ npm run lint # ESLint over src/

| File | Purpose |
|------|---------|
| `src/index.js` | Entry point — orchestrates the upload flow |
| `src/index.js` | Entry point: orchestrates the upload flow |
| `src/archive.js` | Packages test result files into a tar.gz archive |
| `src/upload.js` | Streams the archive to S3 via a signed URL, with retries |
| `src/auth.js` | API token + legacy key/secret authentication |
Expand All @@ -148,6 +148,12 @@ npm run lint # ESLint over src/

The committed `dist/index.js` must stay in sync with `src/`. If you change `src/`, re-run `npm run build` and commit `dist/` in the same PR.

## Writing for this repo

This repository is public and this README is the first thing a prospective user reads, so its prose follows [EXTERNAL-VOICE.md](EXTERNAL-VOICE.md).

The short version: no em-dashes. Rewrite with a full stop, a colon, parentheses, or a comma depending on what the dash was doing, and never a spaced hyphen. The same rule covers the action description in `action.yml` and any log line the action prints into a user's CI output.

## License

MIT. See [LICENSE](LICENSE).
2 changes: 1 addition & 1 deletion dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77957,7 +77957,7 @@ async function run() {
core.notice(
`BuildPulse received this upload. Test results normally appear in the dashboard within a few minutes. ` +
`If you don't see data after ~10 minutes, the most common cause is that the BuildPulse GitHub App ` +
`is not installed on this repository — without it uploads are silently dropped during ingestion. ` +
`is not installed on this repository. Without it, uploads are silently dropped during ingestion. ` +
`Install/grant access at https://github.com/apps/buildpulse and re-run the workflow.`,
{ title: 'BuildPulse upload accepted' }
)
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ async function run() {
core.notice(
`BuildPulse received this upload. Test results normally appear in the dashboard within a few minutes. ` +
`If you don't see data after ~10 minutes, the most common cause is that the BuildPulse GitHub App ` +
`is not installed on this repository — without it uploads are silently dropped during ingestion. ` +
`is not installed on this repository. Without it, uploads are silently dropped during ingestion. ` +
`Install/grant access at https://github.com/apps/buildpulse and re-run the workflow.`,
{ title: 'BuildPulse upload accepted' }
)
Expand Down
Loading