Repository navigation
fix(action): recover the release tags output from git when stdout can't be parsed - #597
Merged
Merged
Conversation
…an't be parsed The action's release-mode outputs (tags/has-changes/version-output) are derived by parsing the CLI's stdout in run-action.mjs. A rare stdout-capture hiccup — the same failure mode that stranded a dist-less action tag — silently empties them, so a consumer gating a downstream step on `outputs.tags` skips it even though the release actually tagged. Recover `tags` from git (`git tag --points-at HEAD`, authoritative) when the parsed output yields none on a real run, and emit a ::warning:: so the underlying stdout issue surfaces instead of a silent empty output. The common path stays a pure parse — git is only consulted as a fallback. Tag selection is extracted into a pure, unit-tested resolveReleaseTags(). Per-package tag formats still rely on parsed output; the comprehensive fix (file-based structured output) is Tier 2. Addresses #594 (Tier 1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6CmYRXTZQYs4MNo6AETxT
Contributor
|
Standing release PR: #595 · v0.41.2 queued (4 packages) · open 52m · ✅ ready to merge Release Preview — v0.41.1 → v0.41.2
These changes will be added to the release PR (#595) when merged: Changelogmonorepo 0.41.1 → 0.41.2
After merge — predicted release
Updated automatically by ReleaseKit |
…on-skip-git Address review on the tags-output recovery: - Only recover when json output was expected (jsonMode) but actually failed to parse (parsed === undefined) — previously the fallback fired for json-off runs (parsed is always undefined there) and even when parsing succeeded with empty tags, populating `tags` from a stale HEAD tag with a misleading warning. - Never recover with --skip-git: no tag is created that run, so a version tag on HEAD would be the previous release's. - resolveReleaseTags now takes an explicit `allowRecovery` flag; tests updated to the `should ...` convention. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6CmYRXTZQYs4MNo6AETxT
This was referenced Jul 21, 2026
Merged
goosewobbler
pushed a commit
that referenced
this pull request
Jul 21, 2026
…tdout Tier 2 of #594. The JSON-result CLI commands (release, gate, standing-pr update/publish) gain a `--output <path>` flag that writes the result JSON to a file via a shared emitResult() helper. run-action passes a temp --output file for those modes and reads the structured result from it (falling back to stdout only if the file is missing, then to Tier 1's git-derived tags). This removes the stdout-scraping fragility from ALL release outputs (has-changes / tags / version-output / release-output), not just `tags` (Tier 1, #597): a stray byte of subprocess or log noise on stdout can no longer corrupt them. Preview mode is unchanged (its markdown output stays on stdout). Addresses #594 (Tier 2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6CmYRXTZQYs4MNo6AETxT
goosewobbler
added a commit
that referenced
this pull request
Jul 21, 2026
…tdout (#605) Tier 2 of #594. The JSON-result CLI commands (release, gate, standing-pr update/publish) gain a `--output <path>` flag that writes the result JSON to a file via a shared emitResult() helper. run-action passes a temp --output file for those modes and reads the structured result from it (falling back to stdout only if the file is missing, then to Tier 1's git-derived tags). This removes the stdout-scraping fragility from ALL release outputs (has-changes / tags / version-output / release-output), not just `tags` (Tier 1, #597): a stray byte of subprocess or log noise on stdout can no longer corrupt them. Preview mode is unchanged (its markdown output stays on stdout). Addresses #594 (Tier 2). Claude-Session: https://claude.ai/code/session_01K6CmYRXTZQYs4MNo6AETxT Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements Tier 1 of #594.
Problem
The Action advertises release-mode outputs (
tags,has-changes,version-output), butrun-action.mjsderives them by parsing the CLI's stdout. A rare stdout-capture hiccup — the same failure mode behind the dist-lessv0.41.1action tag — silently empties them. A user gating a downstream step on the advertised contract:then skips their step even though the release actually tagged — a silent footgun on an output people build automation on.
Fix
When the parsed output yields no tags on a real (non-dry) run, recover them from git —
git tag --points-at HEAD, filtered to version tags — which is authoritative because the release creates the tag at HEAD. A::warning::is emitted on that fallback so the underlying stdout issue surfaces instead of leaving a silently empty output.resolveReleaseTags({ parsedTags, gitTags, dryRun })and unit-tested (prefer-parsed, git-recovery, dry-run-skip, both-empty, missing-parsed).Scope
This is Tier 1 — it hardens the single most load-bearing output (
tags) for the common sync-mode version tag. Per-package tag formats (@scope/pkg@vX.Y.Z) still rely on the parsed output, and the full structured outputs (version-output/release-output) still come from stdout. The comprehensive fix — Tier 2, a CLI--output <file>sorun-action.mjsreads the result from a file instead of capturing stdout — remains tracked in #594 and is left as a follow-up so this change stays small and shippable.Validation
test/integration/action-runner.spec.ts— 5 newresolveReleaseTagscases; full suite green (179 tests).pnpm typecheckclean;biome checkclean (the remaining env-var warnings are pre-existing).🤖 Generated with Claude Code
Greptile Summary
This PR adds a git-based recovery path for the
tagsoutput in release mode: when JSON stdout can't be parsed (a known rare stdout-capture hiccup), the action now runsgit tag --points-at HEADand filters to semver tags rather than silently emitting an emptytagsoutput. The recovery is guarded behindjsonMode && parsed === undefined && !dryRun && !skipGit, and a::warning::annotation is emitted when the fallback fires.gitVersionTagsAtHeadshells out synchronously viaexecFileSynconly on the recovery path (never on the common path), silently returning[]on any git error.resolveReleaseTagsis a pure, exported function that centralises the prefer-parsed / fall-back-to-git / no-recovery logic, covered by 5 new unit tests.jsonmode is off, and stale tags leaking through--skip-git— are addressed by theallowRecoveryguard conditions.Confidence Score: 5/5
Safe to merge — the recovery path is narrowly scoped to cases where JSON parsing failed, and the common path is unchanged.
Both previously flagged issues (recovery firing with json mode off, stale tags leaking through --skip-git) are correctly guarded by the allowRecovery condition. The new resolveReleaseTags function is pure and well-tested. The git call is synchronous but only executes on the rare fallback path, not the normal flow.
No files require special attention.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[writeReleaseOutputs called\nrelease mode, success] --> B{jsonMode?} B -- No --> C[parsed = undefined\nallowRecovery = false] B -- Yes --> D[parseReleaseOutput stdout] D --> E{parse OK?} E -- Yes --> F[parsed = object] E -- No --> G{dryRun or skipGit?} G -- Yes --> H[allowRecovery = false] G -- No --> I[allowRecovery = true\ngitVersionTagsAtHead] F --> J[resolveReleaseTags\nallowRecovery=false] C --> J H --> J I --> J J --> K{parsedTags non-empty\nor allowRecovery=false?} K -- Yes --> L[tags = parsedTags\nrecovered = false] K -- No --> M{gitTags non-empty?} M -- Yes --> N[tags = gitTags\nrecovered = true\nemit warning] M -- No --> O[tags = empty\nrecovered = false] L --> P[setOutput tags] N --> P O --> P%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD A[writeReleaseOutputs called\nrelease mode, success] --> B{jsonMode?} B -- No --> C[parsed = undefined\nallowRecovery = false] B -- Yes --> D[parseReleaseOutput stdout] D --> E{parse OK?} E -- Yes --> F[parsed = object] E -- No --> G{dryRun or skipGit?} G -- Yes --> H[allowRecovery = false] G -- No --> I[allowRecovery = true\ngitVersionTagsAtHead] F --> J[resolveReleaseTags\nallowRecovery=false] C --> J H --> J I --> J J --> K{parsedTags non-empty\nor allowRecovery=false?} K -- Yes --> L[tags = parsedTags\nrecovered = false] K -- No --> M{gitTags non-empty?} M -- Yes --> N[tags = gitTags\nrecovered = true\nemit warning] M -- No --> O[tags = empty\nrecovered = false] L --> P[setOutput tags] N --> P O --> PReviews (2): Last reviewed commit: "fix(action): gate git tag recovery on js..." | Re-trigger Greptile