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
7 changes: 6 additions & 1 deletion communique.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ maintainers validating GitHub Release and npm artifacts.

[defaults]
#model = "claude-opus-4-7"
#max_tokens = 4096
# Communique 1.1.3 caps each model response at 4096 tokens when this is unset.
# One submit_release_notes call (changelog + title + body) needs more than that
# once a release has ~15 commits, so every call was cut off mid-answer and Release
# Please failed with communique::malformed_submission. 16384 is the default that
# Communique 1.2.2 moved to.
max_tokens = 16384
repo = "coder/agent-tty"
#provider = "anthropic"
#base_url = ""
Expand Down
6 changes: 6 additions & 0 deletions docs/RELEASE-PROCESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,12 @@ gh workflow run release-please.yml

Wrong version → land a `Release-As` commit. Wrong notes → fix the merged feature PR bodies (or add `BEGIN_COMMIT_OVERRIDE` blocks) and rerun. Never edit the release PR directly; edits are overwritten.

### Release Please fails with `communique::malformed_submission`

Communique gave up after three `submit_release_notes` calls that lacked a required field (`changelog`, `release_title` or `release_body`). The cause seen so far is the model answer being cut off at Communique's response limit, `max_tokens`. Communique 1.1.3 uses 4096 when `communique.toml` does not set it, and one call needs more than that once a release has about 15 commits. `communique.toml` therefore sets `max_tokens = 16384` under `[defaults]`. A key outside `[defaults]` is silently ignored.

A run log that shows `WARN submit_release_notes was malformed 3 times ... salvaging the last attempt` hit the same cut-off and only passed because the last attempt kept its changelog. If the error returns, raise `max_tokens` before rerunning.

### The release PR merged but no tag or GitHub Release appeared

The Release Please run triggered by the merge failed before `createReleases()` finished. Rerun it — release creation is idempotent (it finds merged release PRs still labeled `autorelease: pending`):
Expand Down
3 changes: 2 additions & 1 deletion src/tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Maintainer tools that run with `tsx` outside the `agent-tty` CLI. They build and
- `applyHeroVideoUrls(options)`: subcommand `apply-video-urls --codex-url URL --claude-url URL [--bundle-dir] [--root-readme]`. Writes the uploaded video URLs into the root `README.md` and the bundle `README.md`. [hero-video-playback.ts:applyHeroVideoUrls]
- `posterConcatFilter`, `replaceVideoSrcs`, `PrepareHeroVideoUploadAssetsOptions`, `ApplyHeroVideoUrlsOptions`: ffmpeg filter and `<video src>` rewrite helpers. [hero-video-playback.ts:posterConcatFilter] [hero-video-playback.ts:replaceVideoSrcs]
- `main` in `release-please-runner.ts` (flag `--dry-run`): runs release-please as a library with the `CommuniqueNodeStrategy` release type and Communique changelog notes. [release-please-runner.ts:main] [release-please-runner.ts:CommuniqueNodeStrategy] [release-please-runner.ts:createCommuniqueChangelogNotes]
- `UnreleasedAwareChangelog`, `formatChangelogSection`, `normalizeCommuniqueBody`, `buildCommuniqueArgs`, `assertLlmCredentials`, `formatReleaseOutputs`, `formatPullRequestOutputs`, `todayIsoDate`, and the types `CommuniqueInvocation`, `CommuniqueRunner`, `RunnerOutputs`: changelog and workflow-output helpers. [release-please-runner.ts:UnreleasedAwareChangelog] [release-please-runner.ts:formatChangelogSection] [release-please-runner.ts:RunnerOutputs]
- `UnreleasedAwareChangelog`, `formatChangelogSection`, `normalizeCommuniqueBody`, `buildCommuniqueArgs`, `describeCommuniqueFailure`, `assertLlmCredentials`, `formatReleaseOutputs`, `formatPullRequestOutputs`, `todayIsoDate`, and the types `CommuniqueInvocation`, `CommuniqueRunner`, `RunnerOutputs`: changelog and workflow-output helpers. [release-please-runner.ts:UnreleasedAwareChangelog] [release-please-runner.ts:formatChangelogSection] [release-please-runner.ts:RunnerOutputs] [release-please-runner.ts:describeCommuniqueFailure]

## Invariants

Expand All @@ -39,6 +39,7 @@ Maintainer tools that run with `tsx` outside the `agent-tty` CLI. They build and
- Promotion validates the manifest with `CanonicalBundleManifestSchema.parse` through `writeCanonicalBundleManifest`. It throws `leak check failed` when `buildLeakFindings` finds account-sensitive patterns in the promoted text files: home paths, emails, the `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_API_KEY` and `OPENAI_API_KEY` names, `API Usage Billing`, `Auth conflict` and `Welcome back` lines, and token-like strings. The check runs on the sanitized texts before promotion clears or writes `--bundle-dir`, so a failure leaves the bundle unchanged. [hero-demo.ts:promote] [hero-demo.ts:buildLeakFindings]
- `applyHeroVideoUrls` asserts that each URL starts with `https://github.com/user-attachments/assets/` and that exactly two `<video src>` tags match. [hero-video-playback.ts:applyHeroVideoUrls] [hero-video-playback.ts:replaceVideoSrcs]
- `assertLlmCredentials` requires `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`, plus `COMMUNIQUE_MODEL` when only the OpenAI key is set. `main` requires `GITHUB_TOKEN` and `GITHUB_REPOSITORY` through `requireEnv`. [release-please-runner.ts:assertLlmCredentials] [release-please-runner.ts:requireEnv]
- `describeCommuniqueFailure` appends a hint that names `max_tokens` when Communique reports `communique::malformed_submission`. `communique.toml` sets `[defaults].max_tokens = 16384` because Communique 1.1.3 cuts each response at 4096 tokens otherwise. [release-please-runner.ts:describeCommuniqueFailure]
- A comment on `formatChangelogSection` states that the heading must not contain a `v` after `[`, because release-please then cannot parse the merged PR. [release-please-runner.ts:formatChangelogSection]

## Side Effects
Expand Down
25 changes: 21 additions & 4 deletions src/tools/release-please-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,26 @@ export type CommuniqueRunner = (
env: NodeJS.ProcessEnv,
) => Promise<void>;

const MALFORMED_SUBMISSION_HINT =
'hint: communique::malformed_submission usually means the model answer was cut ' +
'off at the response token ceiling before it wrote every required field. ' +
'Raise `max_tokens` under [defaults] in communique.toml (Communique 1.1.3 ' +
'uses 4096 when it is unset).';

/**
* Error text for a failed `communique` run: the command line and its stderr,
* plus a hint for the one failure whose message does not name its cause.
*/
export function describeCommuniqueFailure(
args: readonly string[],
stderr: string,
): string {
const message = `communique ${args.join(' ')} failed${stderr === '' ? '' : `:\n${stderr}`}`;
return stderr.includes('communique::malformed_submission')
? `${message}\n${MALFORMED_SUBMISSION_HINT}`
: message;
}

async function runCommuniqueBinary(
args: string[],
env: NodeJS.ProcessEnv,
Expand All @@ -194,10 +214,7 @@ async function runCommuniqueBinary(
error instanceof Object && 'stderr' in error
? String((error as { stderr: unknown }).stderr)
: '';
throw new Error(
`communique ${args.join(' ')} failed${stderr === '' ? '' : `:\n${stderr}`}`,
{ cause: error },
);
throw new Error(describeCommuniqueFailure(args, stderr), { cause: error });
}
}

Expand Down
51 changes: 51 additions & 0 deletions test/unit/tools/release-please-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
assertLlmCredentials,
buildCommuniqueArgs,
createCommuniqueChangelogNotes,
describeCommuniqueFailure,
formatChangelogSection,
formatPullRequestOutputs,
formatReleaseOutputs,
Expand Down Expand Up @@ -391,6 +392,56 @@ describe('createCommuniqueChangelogNotes', () => {
});
});

describe('describeCommuniqueFailure', () => {
const args = ['generate', 'HEAD', 'v0.5.0', '--concise'];

it('points a malformed submission at the max_tokens ceiling', () => {
const stderr = [
'Error: communique::malformed_submission',
' × submit_release_notes was malformed 3 times',
].join('\n');

const message = describeCommuniqueFailure(args, stderr);

expect(message).toContain(
`communique ${args.join(' ')} failed:\n${stderr}`,
);
expect(message).toContain('max_tokens');
expect(message).toContain('communique.toml');
});

it('adds no hint to other failures', () => {
expect(
describeCommuniqueFailure(args, 'Error: ANTHROPIC_API_KEY not set'),
).toBe(
`communique ${args.join(' ')} failed:\nError: ANTHROPIC_API_KEY not set`,
);
expect(describeCommuniqueFailure(args, '')).toBe(
`communique ${args.join(' ')} failed`,
);
});
});

describe('communique.toml', () => {
// Communique 1.1.3 sends 4096 as the response ceiling when this key is
// missing, and cuts every submit_release_notes call off at that size once a
// release has ~15 commits (Release Please then fails with
// communique::malformed_submission). A key outside [defaults] is ignored
// silently, so check the section too.
it('sets a response ceiling above the communique 1.1.3 default in [defaults]', () => {
const toml = readFileSync(
new URL('../../../communique.toml', import.meta.url),
'utf8',
);
const defaults = toml.split(/^\[defaults\]\s*$/m)[1];
expect(defaults).toBeDefined();
const section = (defaults ?? '').split(/^\[/m)[0] ?? '';
const match = /^max_tokens\s*=\s*(\d+)\s*$/m.exec(section);
expect(match).not.toBeNull();
expect(Number(match?.[1])).toBeGreaterThanOrEqual(16384);
});
});

describe('CommuniqueNodeStrategy branch component', () => {
// The branch component follows include-component-in-tag (the stock strategy
// always embeds the package name in the branch). The fake `this` works
Expand Down
Loading