Skip to content

chore(ci): Deploy preview to github - #7353

Open
PeterSchafer wants to merge 1 commit into
mainfrom
chore/CLI-1945_preview_github
Open

PeterSchafer wants to merge 1 commit into
mainfrom
chore/CLI-1945_preview_github

Conversation

@PeterSchafer

@PeterSchafer PeterSchafer commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Pull Request Submission Checklist

  • Follows CONTRIBUTING guidelines
  • Commit messages
    are release-note ready, emphasizing
    what was changed, not how.
  • Includes detailed description of changes
  • Contains risk assessment (Low | Medium | High)
  • Highlights breaking API changes (if applicable)
  • Links to automated tests covering new functionality
  • Includes manual testing instructions (if necessary)
  • Updates relevant GitBook documentation (PR link: ___)
  • Includes product update to be announced in the next stable release notes

What does this PR do?

Additionally deploy preview CLI to github. Since this will create preview tags, they need to be ignored in different release scripts. This is why many scripts are touched as well. I used the chance to ad some tests for pipeline scripts as well so that their behaviour can better be validated.

Where should the reviewer start?

How should this be manually tested?

What's the product update that needs to be communicated to CLI users?


Note

Medium Risk
Changes release pipeline and GitHub/npm “latest” semantics; mistakes could publish wrong channel or block legitimate stable releases, but behavior is covered by new release-script tests.

Overview
Adds preview CLI publishing to GitHub from main (new CircleCI upload github (preview) after S3 preview) and makes release-github deployment-aware so Jira release creation runs only for stable.

Preview/rc GitHub uploads are marked --prerelease so GitHub “latest” stays on stable for Homebrew, Scoop, and install flows. Versioning and pre-release checks now use latest-stable-tag.sh (highest vX.Y.Z, ignoring preview tags) in next-version.sh and create-release.sh, with validate-repository.sh tightened to tags on HEAD and exact stable tag collisions only.

RELEASE_NOTES.md generation passes --skip-unstable to conventional-changelog. CI adds test-release-scripts (make test-release-scripts) and Go tests cover channel mapping, skip-deployment, latest-stable-tag, next-version, and validate-repository behavior.

Reviewed by Cursor Bugbot for commit a966fd7. Bugbot is set up for automated code reviews on this repo. Configure here.

@PeterSchafer
PeterSchafer requested review from a team as code owners October 8, 2026 15:18
@snyk-io

snyk-io Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues
✅ Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@snyk-pr-review-bot

This comment has been minimized.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread release-scripts/upload-artifacts.sh
@PeterSchafer

Copy link
Copy Markdown
Contributor Author

Addressed the Bugbot finding in f6c4c86.

upload_github creates a GitHub release tag (vX.Y.Z-preview.<sha>) for preview builds now. Two other scripts assumed every v* tag was a clean stable semver tag when finding "the latest tag" via an unscoped git describe --tags:

  • release-scripts/create-release.sh uses this to decide whether to cut a new release/X.Y branch — a preview tag being picked as "latest" would break the numeric major/minor/patch comparison.
  • release-scripts/next-version.sh uses it only for a log line, so lower impact, but same bug.

Fixed by scoping both to git describe --tags --match 'v[0-9]*.[0-9]*.[0-9]*' --exclude '*-*', extracted into a shared release-scripts/latest-stable-tag.sh so the pattern lives in one place instead of being duplicated.

@CatalinSnyk

Copy link
Copy Markdown
Contributor

@PeterSchafer sign the commit please 😃

@snyk-pr-review-bot

This comment has been minimized.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread release-scripts/latest-stable-tag.sh Outdated
@PeterSchafer
PeterSchafer force-pushed the chore/CLI-1945_preview_github branch from f6c4c86 to 5f8277a Compare October 8, 2026 16:13
@snyk-pr-review-bot

This comment has been minimized.

@PeterSchafer

Copy link
Copy Markdown
Contributor Author

Fixed the "dirty tag" issue flagged by Bugbot and the PR Reviewer Guide on latest-stable-tag.sh (commit eb1e9f3).

The previous git describe --tags --match ... --exclude ... <rev> call omitted --abbrev=0, so once the most-recently-tagged commit was a preview tag (not itself matching the stable pattern), describe walked back to the nearest stable ancestor and appended -N-gHASH (e.g. v1.2.3-15-gabcdef). create-release.sh then fed that into IFS="." read as the patch version, breaking the numeric comparison.

Switched to the suggested approach:

git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1

This resolves the highest stable tag by semver directly, regardless of which commit it's reachable from — which also fixes the case where the latest stable tag only exists on an unmerged release/* branch.

Verified in a scratch repo with preview tags on top of a stable tag; output is now a clean vX.Y.Z in both cases.

On the "sign the commit" request — I don't have a GPG/SSH signing key configured in this sandbox, so I can't sign commits from here. @PeterSchafer, you may want to sign this commit locally (or re-sign the branch) once you pull it down.

🤖 Generated with Claude Code

@PeterSchafer

Copy link
Copy Markdown
Contributor Author

Fixed the "dirty tag" issue flagged by Bugbot and the PR Reviewer Guide on latest-stable-tag.sh (commit eb1e9f3).

The previous git describe --tags --match ... --exclude ... <rev> call omitted --abbrev=0, so once the most-recently-tagged commit was a preview tag (not itself matching the stable pattern), describe walked back to the nearest stable ancestor and appended -N-gHASH (e.g. v1.2.3-15-gabcdef). create-release.sh then fed that into IFS="." read as the patch version, breaking the numeric comparison.

Switched to the suggested approach:

git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1

This resolves the highest stable tag by semver directly, regardless of which commit it's reachable from — which also fixes the case where the latest stable tag only exists on an unmerged release/* branch.

Verified in a scratch repo with preview tags on top of a stable tag; output is now a clean vX.Y.Z in both cases.

On the "sign the commit" request — I don't have a GPG/SSH signing key configured in this sandbox, so I can't sign commits from here. @PeterSchafer, you may want to sign this commit locally (or re-sign the branch) once you pull it down.

🤖 Generated with Claude Code

@PeterSchafer
PeterSchafer force-pushed the chore/CLI-1945_preview_github branch from eb1e9f3 to d78a826 Compare October 8, 2026 16:20
@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

@PeterSchafer

Copy link
Copy Markdown
Contributor Author

Fixed the CI failure (make: *** [Makefile:57: binary-releases/version] Error 141) in commit abf1b4f.

Root cause: latest-stable-tag.sh piped into head -n 1:

git tag --list '...' --sort=-version:refname | grep -E '...' | head -n 1

With enough tags, head can close its end of the pipe as soon as it has its one line, before git tag/grep finish writing the rest. Under set -o pipefail that SIGPIPE on the upstream process surfaces as the pipeline's exit status — 141 (128+SIGPIPE) — even though the correct tag had already been printed. It's a race, which is why it didn't fail every time.

Fix: capture the full filtered tag list into a variable first, then take the first line in bash (${stable_tags%%$'\n'*}) instead of piping through head. Also added an explicit "No stable release tags found" error path for the (currently theoretical) case of no stable tags existing.

Verified locally by stress-testing the old pipeline against a repo with 20k tags (reproduced exit 141 intermittently) and confirming the new version returns the correct tag consistently across 50 runs, plus the empty-tags edge case.

🤖 Generated with Claude Code

@PeterSchafer
PeterSchafer force-pushed the chore/CLI-1945_preview_github branch from abf1b4f to a8e4efd Compare October 8, 2026 16:30
@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread release-scripts/next-version.sh
@PeterSchafer
PeterSchafer force-pushed the chore/CLI-1945_preview_github branch from a8e4efd to 736eb7d Compare October 8, 2026 19:08
@snyk-pr-review-bot

This comment has been minimized.

@PeterSchafer
PeterSchafer force-pushed the chore/CLI-1945_preview_github branch from 736eb7d to 507386d Compare October 9, 2026 08:25
@snyk-pr-review-bot

This comment has been minimized.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread .circleci/config.yml
@PeterSchafer
PeterSchafer force-pushed the chore/CLI-1945_preview_github branch from 507386d to c117d09 Compare October 9, 2026 09:34
@snyk-pr-review-bot

This comment has been minimized.

@PeterSchafer
PeterSchafer force-pushed the chore/CLI-1945_preview_github branch from c117d09 to 714247a Compare October 9, 2026 09:59
@snyk-pr-review-bot

This comment has been minimized.

@PeterSchafer
PeterSchafer force-pushed the chore/CLI-1945_preview_github branch from 714247a to c5e4361 Compare October 9, 2026 10:17
@snyk-pr-review-bot

This comment has been minimized.

@PeterSchafer
PeterSchafer enabled auto-merge October 9, 2026 13:28
@PeterSchafer
PeterSchafer force-pushed the chore/CLI-1945_preview_github branch from c5e4361 to 34f7afe Compare October 11, 2026 09:40
@snyk-pr-review-bot

This comment has been minimized.

Comment thread release-scripts/create-release.sh Outdated
echo "Checking for new versions..."

latest_version_full=$(git describe --tags `git rev-list --tags --max-count=1`)
latest_version_full=$(./release-scripts/latest-stable-tag.sh)

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.

In Makefile line 109, it builds the RELEASE_NOTES.md with:

$(BINARY_OUTPUT_FOLDER)/RELEASE_NOTES.md:
	npx conventional-changelog-cli -l -r 1 -n ./release-scripts/conventional-changelog-cli-config.js > $(BINARY_OUTPUT_FOLDER)/RELEASE_NOTES.md

I believe it is something to take a look too.

Comment thread release-scripts/latest-stable-tag.sh Outdated
Comment on lines +4 to +21
# Prints the highest stable release tag (vX.Y.Z, no pre-release suffix) by
# semver, ignoring preview/rc tags and independent of which commit they are
# reachable from (so this also works for a stable tag that only exists on an
# unmerged release/* branch).
#
# Deliberately not piped through `head -n 1`: with enough tags, head can
# close the pipe before git/grep finish writing, and under `pipefail` the
# resulting SIGPIPE surfaces as a build failure even though the correct tag
# was already produced.

stable_tags="$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' || true)"

if [ -z "$stable_tags" ]; then
echo "No stable release tags found" >&2
exit 1
fi

echo "${stable_tags%%$'\n'*}"

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.

does these "stable_tags" also includes the hotfix releases?
Wondering about this name "stable_tags", if should be something else

Comment thread release-scripts/skip-deployment_test.go Outdated
Comment on lines +18 to +24
// TestSkipDeploymentRunsOnlyForMatchingChannel is a regression test for the
// deployment gate: a CircleCI job must run (skip=false) only when the
// requested channel matches the branch's actual, git-derived release
// channel, and must be skipped (skip=true) for every other channel. This is
// the only thing standing between the CircleCI `deployment` parameter and
// the branch-derived RELEASE_CHANNEL disagreeing with each other.
func TestSkipDeploymentRunsOnlyForMatchingChannel(t *testing.T) {

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.

please remove these comments and others that have been added in this PR. I know LLM adds a lot of these comments, but the code should be readable by itself. These comments are often confusing or can cause maintenance burden in the future.

@danskmt danskmt Oct 12, 2026 •

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.

The changes in this file seems to be unrelated to this PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The tests now run as a part of the pipeline and were failing.

# the "latest release" pointer consumed by homebrew-tap, scoop-snyk, and install
# scripts keeps resolving to stable only.
local prerelease_arg=""
if [ -n "${RELEASE_CHANNEL}" ] && [ "${RELEASE_CHANNEL}" != "stable" ]; then

@danskmt danskmt Oct 12, 2026 •

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.

this check is being performed in two places (line 119).
Suggestion: Extract a small is_stable_release helper.

Comment thread release-scripts/latest-stable-tag.sh Outdated
Comment on lines +4 to +14
# Prints the highest stable release tag (vX.Y.Z, no pre-release suffix) by
# semver, ignoring preview/rc tags and independent of which commit they are
# reachable from (so this also works for a stable tag that only exists on an
# unmerged release/* branch).
#
# Deliberately not piped through `head -n 1`: with enough tags, head can
# close the pipe before git/grep finish writing, and under `pipefail` the
# resulting SIGPIPE surfaces as a build failure even though the correct tag
# was already produced.

stable_tags="$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' || 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.

I guess these comments can be shortened. Possibly the second part of it can be removed, WDYT?

Comment thread release-scripts/create-release.sh Outdated
echo "Checking for new versions..."

latest_version_full=$(git describe --tags `git rev-list --tags --max-count=1`)
latest_version_full=$(./release-scripts/latest-stable-tag.sh)

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.

Similar to next-version.sh, should it make use of $(dirname "$0")/latest-stable-tag.sh instead of the relative path here?

Comment on lines +40 to +72
func TestLatestStableTagIgnoresMoreRecentPreviewTags(t *testing.T) {
dir := newTagTestRepo(t)
tagCommit(t, dir, "v1.0.0")
newCommit(t, dir, "preview 1")
tagCommit(t, dir, "v1.1.0-preview.deadbeef")
newCommit(t, dir, "preview 2")
tagCommit(t, dir, "v1.1.0-preview.cafebabe")

got, err := runLatestStableTag(t, dir)
if err != nil {
t.Fatalf("expected success, got error: %v\noutput: %s", err, got)
}
if got != "v1.0.0" {
t.Fatalf("expected clean stable tag v1.0.0, got %q", got)
}
}

func TestLatestStableTagPicksHighestSemverNotMostRecentlyCreated(t *testing.T) {
dir := newTagTestRepo(t)
// Tags created out of semver order, mirroring a patch release cut from an
// older branch after a newer minor was already tagged.
tagCommit(t, dir, "v1.2.0")
newCommit(t, dir, "c2")
tagCommit(t, dir, "v1.1.5")

got, err := runLatestStableTag(t, dir)
if err != nil {
t.Fatalf("expected success, got error: %v\noutput: %s", err, got)
}
if got != "v1.2.0" {
t.Fatalf("expected highest stable tag v1.2.0, got %q", got)
}
}

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.

Suggestion: some tests can be converted to a table-testing format

Comment on lines +87 to +105
// TestLatestStableTagSurvivesLargeTagCountUnderPipefail is a regression test
// for a SIGPIPE race: piping the tag list through `head -n 1` let head close
// the pipe before git/grep finished writing once there were enough tags,
// which `set -o pipefail` turned into an intermittent build failure (exit
// 141) even though the correct tag had already been produced.
func TestLatestStableTagSurvivesLargeTagCountUnderPipefail(t *testing.T) {
if testing.Short() {
t.Skip("skipping large tag count test in short mode")
}

dir := newTagTestRepo(t)
const tagCount = 20000

// git tag only accepts one tag per invocation; batch creation via
// update-ref is far faster than tagCount separate `git tag` calls.
var stdin bytes.Buffer
for i := 1; i <= tagCount; i++ {
stdin.WriteString("create refs/tags/v1.0." + strconv.Itoa(i) + " HEAD\n")
}

@danskmt danskmt Oct 12, 2026 •

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.

Do we need this test? It creates 20k tags and runs the script 20 times on every CI run, which makes the test suite slower. The SIGPIPE race it covers is timing-dependent, so it could still pass if the bug came back. I'd lower the numbers or remove it.

Comment thread .circleci/config.yml
Comment on lines +658 to +666
- test-release-scripts:
context:
- devex_cli_docker_hub
requires:
- prepare-build
filters:
branches:
ignore:
- '/^docs\/.*/'

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.

test-release-scripts isn't in any release job's requires, so a failure doesn't block deploys. Is this intended?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

release jobs run on release branches, tests run on non release branches. a change to the scripts needs to go through a non release branch and thereby tests are executed. This is true for all tests.

@PeterSchafer
PeterSchafer force-pushed the chore/CLI-1945_preview_github branch from 34f7afe to a996329 Compare October 12, 2026 11:09
@snyk-pr-review-bot

This comment has been minimized.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a996329. Configure here.

Comment thread release-scripts/upload-artifacts.sh
Comment thread release-scripts/validate-repository.sh
@@ -97,6 +106,7 @@ upload_github() {
else
echo "Uploading to GitHub..."

@octavian-snyk octavian-snyk Oct 12, 2026 •

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.

Should we update the logic to check if the release exists before attempting to create or upload?

Perhaps:

if gh release view "${VERSION_TAG}" >/dev/null 2>&1; then
        echo "Release ${VERSION_TAG} already exists, uploading assets..."
        gh release upload "${VERSION_TAG}" "${StaticFiles[@]}" --clobber
      else
        echo "Uploading to GitHub..."
        gh release create "${VERSION_TAG}" "${StaticFiles[@]}" \
          ${prerelease_arg} \
          --target "${CIRCLE_SHA1}" \
          --title "${VERSION_TAG}" \
          --notes-file binary-releases/RELEASE_NOTES.md
      fi

GitHub API hiccups can occasionally cause a release creation step to fail midway or fail to register as complete. Currently, if this step fails, re-running the job fails because the release tag already exists, requiring someone to manually delete the release before re-triggering. This has happened on other releases 😅

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I would say that this would increase the scope of the task too much.

@PeterSchafer
PeterSchafer force-pushed the chore/CLI-1945_preview_github branch from a996329 to 2d5a9e9 Compare October 12, 2026 13:11
@snyk-pr-review-bot

This comment has been minimized.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@PeterSchafer
PeterSchafer force-pushed the chore/CLI-1945_preview_github branch from 2d5a9e9 to a966fd7 Compare October 12, 2026 14:12
@snyk-pr-review-bot

Copy link
Copy Markdown

PR Reviewer Guide 🔍

🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected
📚 Repository Context Analyzed

This review considered 25 relevant code sections from 11 files (average relevance: 0.80)

🤖 Repository instructions applied (from AGENTS.md)

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.

4 participants