Skip to content

Add automated rollback logic to publish workflow and update TypeScript settings#109

Merged
utkarsh232005 merged 2 commits into
KDM-cli:mainfrom
utkarsh232005:main
May 28, 2026
Merged

Add automated rollback logic to publish workflow and update TypeScript settings#109
utkarsh232005 merged 2 commits into
KDM-cli:mainfrom
utkarsh232005:main

Conversation

@utkarsh232005
Copy link
Copy Markdown
Member

@utkarsh232005 utkarsh232005 commented May 28, 2026

Summary by CodeRabbit

  • Chores
    • Enhanced release process reliability with automatic rollback capability on deployment failures.

Review Change Stack

@utkarsh232005 utkarsh232005 marked this pull request as ready for review May 28, 2026 20:01
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key: "pre_merge_checks"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

Walkthrough

The publish workflow now includes a failure-triggered rollback step. When the release job fails, the step reads the version from package.json, constructs the release tag, deletes the remote tag, and conditionally force-pushes or reverts the main branch to restore pre-release state.

Changes

Release rollback on failure

Layer / File(s) Summary
Rollback release on failure step
.github/workflows/publish.yml
New failure-only workflow step that reverses release effects by deleting the remote tag and conditionally restoring the main branch via force-push or revert commits, with logic to detect release commit presence and HEAD position.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Rationale: Shell command logic is high-density with multiple git operations, conditional branches, and string substitution from workflow context. Security considerations around command injection, git authorization, and branch history mutations require careful inspection. The step's role in disaster recovery adds criticality despite its single-file footprint.

Possibly related PRs

  • KDM-cli/kdm-cli#45: Directly related workflow modification; both PRs alter the publish/release pipeline in .github/workflows/publish.yml.

Suggested labels

ci/cd

Poem

🚨 When releases fail and chaos blooms,
This step sweeps clean the git-stained rooms—
Tags deleted, branches mended fast,
Main restored to what it was last. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title mentions 'Add automated rollback logic to publish workflow' which directly corresponds to the main change (conditional rollback step in publish.yml), and also references 'update TypeScript settings' which is not present in the changeset. Remove the 'update TypeScript settings' portion from the title, as the changeset only modifies the publish workflow with rollback logic. Consider: 'Add automated rollback logic to publish workflow'.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the ci/cd label May 28, 2026
Copy link
Copy Markdown

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Choose a reason for hiding this comment

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

Our agent can fix these. Install it.

No application code in the PR — skipped Code Health checks.

Quality Gate Profile: The Bare Minimum
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

@utkarsh232005 utkarsh232005 merged commit d5ef41d into KDM-cli:main May 28, 2026
17 of 18 checks passed
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/publish.yml (1)

53-56: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

npm publish is irreversible; rollback leaves published package on registry.

The rollback step triggers if: failure(), but once npm publish succeeds (line 54), subsequent step failures (e.g., GitHub Release creation) will initiate rollback. However, npm packages cannot be unpublished after 72 hours, and even within that window, unpublishing is discouraged. This creates an inconsistent state: the tag and commit are rolled back, but the package version exists on npm.

Consider restructuring the workflow to minimize this window—e.g., create the GitHub Release before publishing to npm, or add an explicit npm unpublish attempt in the rollback (with appropriate caveats about npm's deprecation policy).

Also applies to: 72-73

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 53 - 56, The workflow currently
runs the "Publish to npm" step (npm publish --access public) before creating the
GitHub Release and relies on a rollback triggered by if: failure(), which can
leave the package published even if other steps fail; change the job order so
the GitHub Release (and any other post-release validations) are created/verified
before the "Publish to npm" step (make "Publish to npm" the last step), or
alternatively add an explicit rollback action that attempts npm unpublish (with
a clear warning/comment about npm's unpublish policy) in the failure
handler—refer to the "Publish to npm" step name and the failure() conditional to
locate where to reorder or add the npm-unpublish rollback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/publish.yml:
- Around line 76-77: Duplicate version extraction: replace the local shell node
command that reads package.json (the VERSION and TAG variables) with the
previously computed get_version step output by using
steps.get_version.outputs.VERSION to set VERSION (and then TAG="v$VERSION"), and
ensure the job/step that uses this runs in a context where step outputs are
preserved (confirm behavior when using failure() context in your CI); update any
environment variable references to use that output rather than re-parsing
package.json.
- Around line 72-116: The rollback script (job step "Rollback release on
failure") lacks signal handling and can leave the repo in a partial state if
canceled mid-run; add a shell trap for SIGINT and SIGTERM at the start of the
step that records current state (e.g., TAG, RELEASE_SHA, REMOTE_SHA), sets a
flag, and runs a cleanup handler which attempts to revert any partial actions
(for example, re-push a deleted tag or abort/restore working tree with git reset
--hard and re-checkout origin/main) before exiting; ensure the trap wraps the
main logic around the variables TAG and RELEASE_SHA and that all git operations
check for the cleanup-flag so the handler can safely restore or bail out
cleanly.
- Around line 94-102: The push logic has a TOCTOU race: after `git fetch origin
main` and checking `REMOTE_SHA` == `RELEASE_SHA`, another commit could land
before `git push origin HEAD~1:main --force`; update the branch protection by
either re-fetching the remote ref immediately before pushing or (preferably)
replace the unconditional `git push origin HEAD~1:main --force` with a safe push
using `--force-with-lease` so the push will only succeed if `origin/main` still
equals `REMOTE_SHA`; ensure references to `REMOTE_SHA`, `RELEASE_SHA`, the `git
fetch origin main` step, and the `git push origin HEAD~1:main --force` command
are adjusted accordingly.
- Around line 84-85: When deleting the tag you also need to delete the GitHub
Release object created by softprops/action-gh-release@v3 so it doesn't remain
orphaned; after the existing tag deletion step (git push origin
:refs/tags/"$TAG") call the GitHub API/gh CLI to remove the release matching
"$TAG" (e.g., use gh release delete "$TAG" --yes or the REST API with
GITHUB_TOKEN) and ensure the workflow has permission to delete releases so both
the tag and the release are removed.
- Around line 99-101: When the force-push fallback runs, first sync local with
remote and explicitly revert the original release commit instead of assuming
HEAD: run git fetch origin, then verify that $RELEASE_SHA is still an ancestor
of origin/main (git merge-base --is-ancestor $RELEASE_SHA origin/main); if the
check passes run git checkout -B revert-fallback origin/main && git revert
--no-edit $RELEASE_SHA && git push origin HEAD:main, otherwise fail the job with
a clear error message so you don't attempt a non-fast-forward push; reference
the variables/refs RELEASE_SHA, origin/main and the git revert command in the
workflow.

---

Outside diff comments:
In @.github/workflows/publish.yml:
- Around line 53-56: The workflow currently runs the "Publish to npm" step (npm
publish --access public) before creating the GitHub Release and relies on a
rollback triggered by if: failure(), which can leave the package published even
if other steps fail; change the job order so the GitHub Release (and any other
post-release validations) are created/verified before the "Publish to npm" step
(make "Publish to npm" the last step), or alternatively add an explicit rollback
action that attempts npm unpublish (with a clear warning/comment about npm's
unpublish policy) in the failure handler—refer to the "Publish to npm" step name
and the failure() conditional to locate where to reorder or add the
npm-unpublish rollback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 26747428-80dc-41c3-9df8-370ce5a10f35

📥 Commits

Reviewing files that changed from the base of the PR and between db67083 and 389498c.

⛔ Files ignored due to path filters (1)
  • tsconfig.json is excluded by none and included by none
📒 Files selected for processing (1)
  • .github/workflows/publish.yml

Comment on lines +72 to +116
- name: Rollback release on failure
if: failure()
shell: bash
run: |
VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('./package.json', 'utf8')).version)")
TAG="v$VERSION"
RELEASE_SHA=$(git rev-parse HEAD)
COMMIT_MSG=$(git log -1 --pretty=%B "$RELEASE_SHA")

if echo "$COMMIT_MSG" | grep -q "chore: release v$VERSION"; then
echo "Workflow failed. Attempting to roll back release $TAG..."

# Delete remote tag if it exists
git push origin :refs/tags/"$TAG" || true

# Fetch latest remote changes
git fetch origin main
REMOTE_SHA=$(git rev-parse origin/main)

# Ensure local working directory is clean
git reset --hard HEAD

if [ "$REMOTE_SHA" = "$RELEASE_SHA" ]; then
echo "Release commit is the latest on origin/main. Force-pushing HEAD~1..."
if git push origin HEAD~1:main --force; then
echo "Rollback successful: release commit removed."
else
echo "Force-push failed (likely due to branch protection). Creating a revert commit instead..."
git revert --no-edit HEAD
git push origin HEAD:main
fi
else
if git merge-base --is-ancestor "$RELEASE_SHA" origin/main; then
echo "Release commit exists on origin/main but is not the latest. Reverting it..."
git checkout main
git pull origin main
git revert --no-edit "$RELEASE_SHA"
git push origin HEAD:main
else
echo "Release commit was not pushed to origin/main. No rollback needed."
fi
fi
else
echo "No release commit found on HEAD. No rollback needed."
fi
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.

🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

No handling for workflow cancellation mid-rollback.

If the workflow is cancelled while the rollback script is executing, the repository could be left in a partially rolled-back state (e.g., tag deleted but branch not reverted). Consider adding a trap to handle SIGTERM/SIGINT gracefully, or at minimum, document this risk.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 72 - 116, The rollback script
(job step "Rollback release on failure") lacks signal handling and can leave the
repo in a partial state if canceled mid-run; add a shell trap for SIGINT and
SIGTERM at the start of the step that records current state (e.g., TAG,
RELEASE_SHA, REMOTE_SHA), sets a flag, and runs a cleanup handler which attempts
to revert any partial actions (for example, re-push a deleted tag or
abort/restore working tree with git reset --hard and re-checkout origin/main)
before exiting; ensure the trap wraps the main logic around the variables TAG
and RELEASE_SHA and that all git operations check for the cleanup-flag so the
handler can safely restore or bail out cleanly.

Comment on lines +76 to +77
VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('./package.json', 'utf8')).version)")
TAG="v$VERSION"
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.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Version extraction duplicated; consider reusing step output.

The version is already extracted in the get_version step (lines 58-62). You could reference ${{ steps.get_version.outputs.VERSION }} via an environment variable instead of re-parsing package.json. However, since failure() context might not preserve all step outputs reliably, verify this works in your CI environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 76 - 77, Duplicate version
extraction: replace the local shell node command that reads package.json (the
VERSION and TAG variables) with the previously computed get_version step output
by using steps.get_version.outputs.VERSION to set VERSION (and then
TAG="v$VERSION"), and ensure the job/step that uses this runs in a context where
step outputs are preserved (confirm behavior when using failure() context in
your CI); update any environment variable references to use that output rather
than re-parsing package.json.

Comment on lines +84 to +85
# Delete remote tag if it exists
git push origin :refs/tags/"$TAG" || true
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

GitHub Release artifact is not deleted, only the tag.

The softprops/action-gh-release@v3 action creates a GitHub Release object. Deleting the tag via git push origin :refs/tags/"$TAG" leaves an orphaned release pointing to a non-existent tag. You should also delete the release itself using the GitHub API.

🛠️ Proposed fix to delete the GitHub Release
             # Delete remote tag if it exists
             git push origin :refs/tags/"$TAG" || true
+            
+            # Delete GitHub Release if it exists
+            RELEASE_ID=$(gh release view "$TAG" --json id -q '.id' 2>/dev/null || true)
+            if [ -n "$RELEASE_ID" ]; then
+              gh release delete "$TAG" --yes || echo "Failed to delete GitHub Release"
+            fi

Note: This requires the gh CLI and appropriate GITHUB_TOKEN permissions (already present).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Delete remote tag if it exists
git push origin :refs/tags/"$TAG" || true
# Delete remote tag if it exists
git push origin :refs/tags/"$TAG" || true
# Delete GitHub Release if it exists
RELEASE_ID=$(gh release view "$TAG" --json id -q '.id' 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
gh release delete "$TAG" --yes || echo "Failed to delete GitHub Release"
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 84 - 85, When deleting the tag
you also need to delete the GitHub Release object created by
softprops/action-gh-release@v3 so it doesn't remain orphaned; after the existing
tag deletion step (git push origin :refs/tags/"$TAG") call the GitHub API/gh CLI
to remove the release matching "$TAG" (e.g., use gh release delete "$TAG" --yes
or the REST API with GITHUB_TOKEN) and ensure the workflow has permission to
delete releases so both the tag and the release are removed.

Comment on lines +94 to +102
if [ "$REMOTE_SHA" = "$RELEASE_SHA" ]; then
echo "Release commit is the latest on origin/main. Force-pushing HEAD~1..."
if git push origin HEAD~1:main --force; then
echo "Rollback successful: release commit removed."
else
echo "Force-push failed (likely due to branch protection). Creating a revert commit instead..."
git revert --no-edit HEAD
git push origin HEAD:main
fi
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

TOCTOU race between fetch and force-push.

Between git fetch origin main (line 88) and the force-push (line 96), another commit could land on origin/main. The $REMOTE_SHA == $RELEASE_SHA check would pass based on stale data, but the force-push would then overwrite unrelated commits. Consider re-fetching immediately before the push or using --force-with-lease to ensure the expected ref hasn't moved.

🔒 Proposed fix using --force-with-lease
-              if git push origin HEAD~1:main --force; then
+              if git push origin HEAD~1:main --force-with-lease=main:"$REMOTE_SHA"; then

This ensures the push only succeeds if origin/main still points to $REMOTE_SHA.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 94 - 102, The push logic has a
TOCTOU race: after `git fetch origin main` and checking `REMOTE_SHA` ==
`RELEASE_SHA`, another commit could land before `git push origin HEAD~1:main
--force`; update the branch protection by either re-fetching the remote ref
immediately before pushing or (preferably) replace the unconditional `git push
origin HEAD~1:main --force` with a safe push using `--force-with-lease` so the
push will only succeed if `origin/main` still equals `REMOTE_SHA`; ensure
references to `REMOTE_SHA`, `RELEASE_SHA`, the `git fetch origin main` step, and
the `git push origin HEAD~1:main --force` command are adjusted accordingly.

Comment on lines +99 to +101
echo "Force-push failed (likely due to branch protection). Creating a revert commit instead..."
git revert --no-edit HEAD
git push origin HEAD:main
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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Revert fallback after failed force-push doesn't sync with remote first.

If force-push fails due to branch protection (line 96), the fallback immediately runs git revert --no-edit HEAD on the local state without pulling the latest remote. If branch protection is enabled, there may also be additional commits on origin/main that arrived after git fetch. The subsequent git push origin HEAD:main will fail as a non-fast-forward push.

🐛 Proposed fix to sync before revert
               else
                 echo "Force-push failed (likely due to branch protection). Creating a revert commit instead..."
+                git fetch origin main
+                git checkout main
+                git reset --hard origin/main
                 git revert --no-edit HEAD
-                git push origin HEAD:main
+                git revert --no-edit "$RELEASE_SHA"
+                git push origin main
               fi

Wait—after reset to origin/main, the release commit may no longer be HEAD. The revert target should be $RELEASE_SHA explicitly, and you need to verify it's still an ancestor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 99 - 101, When the force-push
fallback runs, first sync local with remote and explicitly revert the original
release commit instead of assuming HEAD: run git fetch origin, then verify that
$RELEASE_SHA is still an ancestor of origin/main (git merge-base --is-ancestor
$RELEASE_SHA origin/main); if the check passes run git checkout -B
revert-fallback origin/main && git revert --no-edit $RELEASE_SHA && git push
origin HEAD:main, otherwise fail the job with a clear error message so you don't
attempt a non-fast-forward push; reference the variables/refs RELEASE_SHA,
origin/main and the git revert command in the workflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant