Skip to content

fix(action): recover the release tags output from git when stdout can't be parsed - #597

Merged
goosewobbler merged 2 commits into
mainfrom
fix/action-tags-output-from-git
Jul 21, 2026
Merged

goosewobbler merged 2 commits into
mainfrom
fix/action-tags-output-from-git

Conversation

@goosewobbler

@goosewobbler goosewobbler commented Jul 20, 2026 •

Copy link
Copy Markdown
Owner

Implements Tier 1 of #594.

Problem

The Action advertises release-mode outputs (tags, has-changes, version-output), but run-action.mjs derives them by parsing the CLI's stdout. A rare stdout-capture hiccup — the same failure mode behind the dist-less v0.41.1 action tag — silently empties them. A user gating a downstream step on the advertised contract:

- uses: goosewobbler/releasekit@vX
  id: release
  with: { mode: release }
- if: steps.release.outputs.tags != ''
  run: ./promote "${{ steps.release.outputs.tags }}"

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.

  • Common path unchanged: when stdout parses (the normal case), the parsed tags are used and git is never consulted.
  • Pure + tested: the selection logic is extracted into resolveReleaseTags({ parsedTags, gitTags, dryRun }) and unit-tested (prefer-parsed, git-recovery, dry-run-skip, both-empty, missing-parsed).
  • Dry run: never shells out to git (no tags exist); uses the parsed/intended tags.

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> so run-action.mjs reads 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 new resolveReleaseTags cases; full suite green (179 tests).
  • pnpm typecheck clean; biome check clean (the remaining env-var warnings are pre-existing).

🤖 Generated with Claude Code

Greptile Summary

This PR adds a git-based recovery path for the tags output in release mode: when JSON stdout can't be parsed (a known rare stdout-capture hiccup), the action now runs git tag --points-at HEAD and filters to semver tags rather than silently emitting an empty tags output. The recovery is guarded behind jsonMode && parsed === undefined && !dryRun && !skipGit, and a ::warning:: annotation is emitted when the fallback fires.

  • gitVersionTagsAtHead shells out synchronously via execFileSync only on the recovery path (never on the common path), silently returning [] on any git error.
  • resolveReleaseTags is a pure, exported function that centralises the prefer-parsed / fall-back-to-git / no-recovery logic, covered by 5 new unit tests.
  • Both previously-raised review concerns — recovery firing when json mode is off, and stale tags leaking through --skip-git — are addressed by the allowRecovery guard 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

Filename Overview
scripts/run-action.mjs Adds gitVersionTagsAtHead (sync fallback via execFileSync) and resolveReleaseTags (pure tag selector); writeReleaseOutputs now guards recovery behind jsonMode && parsed === undefined && !dryRun && !skipGit, correctly addressing both prior review concerns.
scripts/run-action.d.mts Exports resolveReleaseTags with allowRecovery?: boolean typed as optional — consistent with the implementation treating undefined as falsy (no recovery).
test/integration/action-runner.spec.ts Adds 5 unit tests for resolveReleaseTags covering: prefer-parsed, git-recovery, missing-parsed, no-recovery-when-disallowed, and both-empty; all cases are well-specified.

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
Loading
%%{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 --> P
Loading

Reviews (2): Last reviewed commit: "fix(action): gate git tag recovery on js..." | Re-trigger Greptile

…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
@github-actions

github-actions Bot commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

Standing release PR: #595 · v0.41.2 queued (4 packages) · open 52m · ✅ ready to merge

Release Preview — v0.41.1 → v0.41.2

Note: Labels on this PR are advisory in standing-pr mode. Bumps come from conventional commits in the standing PR; override by editing labels on the standing PR itself. Add release:immediate to bypass the standing PR and release this PR directly.

These changes will be added to the release PR (#595) when merged:

Changelog

monorepo 0.41.1 → 0.41.2

Fixed

  • gate git tag recovery on json mode, parse failure, and non-skip-git (action)
  • recover the release tags output from git when stdout can't be parsed (action)

All packages → 0.41.2 (sync versioning)

  • @releasekit/version
  • @releasekit/notes
  • @releasekit/publish
  • @releasekit/release

After merge — predicted release

No version escalation — this PR's changes will be included in the queued release without affecting the projected versions.

Package Standing PR This PR After merge
monorepo 0.41.2 0.41.2 0.41.2

Updated automatically by ReleaseKit

Comment thread scripts/run-action.mjs Outdated
Comment thread scripts/run-action.mjs Outdated
Comment thread scripts/run-action.mjs
…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
@goosewobbler
goosewobbler merged commit 6e5197b into main Jul 21, 2026
28 checks passed
@goosewobbler
goosewobbler deleted the fix/action-tags-output-from-git branch July 21, 2026 00:52
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>
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.

1 participant