Skip to content

ci(release): publish from the merge commit, without a release commit - #409

Merged
saeedjamshaid merged 8 commits into
devfrom
saeedjamshaid/release-pipeline
Oct 5, 2026
Merged

saeedjamshaid merged 8 commits into
devfrom
saeedjamshaid/release-pipeline

Conversation

@saeedjamshaid

@saeedjamshaid saeedjamshaid commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A merge into main or beta becomes the whole release. semantic-release no longer commits CHANGELOG.md and package.json back to the release branch. So no ruleset has to be switched off around a release, the branches stop drifting apart, and the release notes live in GitHub Releases. After a stable release or a hotfix, the release run merges main back into dev by itself. The alpha branch is gone from the configuration: nothing has shipped on it since 1.1.0-alpha.22.

The plan is .ai/plans/release-pipeline.md (the first commit). This PR is its step 7.2; the rest are admin settings and release-day steps, listed at the end.

How releasing works after this PR

flowchart LR
  F["feature / fix branch"] -- squash --> D[dev]
  D -- merge commit --> B[beta]
  B -- merge commit --> M[main]
  H["hotfix branch<br/>cut from main"] -- merge commit --> M
  M -. "back-merge by the release run<br/>(merge commit)" .-> D
  B --> NB[("npm @beta")]
  M --> NL[("npm @latest")]
Loading
Branch Takes PRs from Merge with A merge publishes
dev feature and fix branches; the release run's back-merge from main squash only; a back-merge is a merge commit the run pushes past the ruleset nothing
beta dev only merge commit x.y.z-beta.N to beta, and a GitHub prerelease
main beta; hotfix branches cut from main merge commit x.y.z to latest, and the GitHub release
1.x (only if a 1.x patch is ever needed) fix branches cut from 1.x merge commit 1.x.y to release-1.x

A release branch never takes a squash or a rebase. semantic-release reads every commit on it to choose the version and write the notes, so a squashed promotion either releases nothing or lists a change twice. The only squashes happen on the way into dev: one PR is one commit, and one line in the notes. And dev takes nothing but squashes: a feature PR merged with a merge commit would land commits nobody linted, and the next promotion would read every one of them.

Day to day

  • Feature or fix: branch from dev, open a PR, and Squash and merge. The PR title becomes the commit header, which decides the release and its line in the notes:

    • fix(portal): … releases a patch and feat(sdk): … a minor;
    • feat(sdk)!: … is a breaking change, a major;
    • revert(scope): … is a revert, a patch;
    • docs:, chore:, refactor:, test:, ci: and build: release nothing.

    The description stays in the commit's history. Nothing in it reaches the notes or changes the version, and a BREAKING CHANGE: footer in it is refused unless the title carries !.

  • Beta: open a PR dev → beta and Create a merge commit. The run publishes the prerelease, with notes covering what changed since the previous one.

  • Stable: open a PR beta → main and Create a merge commit. The run publishes to latest, with notes covering everything since the previous stable release, and then merges main back into dev with a merge commit. The release tag sits on main's merge commit, and a beta cut from a dev that cannot reach it would get the wrong version, so dev → beta is refused until dev contains main.

  • Hotfix:

    1. Branch from main, make one conventional commit, and open a PR into main.
    2. Merge it with a merge commit; that is a patch on latest. The run back-merges it into dev.
    3. Promote dev → beta before the next beta → main.
  • A back-merge the run could not make (its back-merge job failed on a conflict): GitHub's "Resolve conflicts" button would commit to main, which is blocked. Until it is done, dev → beta stays refused. Instead:

    1. Cut <name>/merge-main from dev.
    2. Merge origin/main into it, resolve the conflicts, and open the PR into dev.
    3. An admin adds merge commit to dev's ruleset, merges the PR with Create a merge commit, and sets the ruleset back to squash only. Until then the merge button offers only Squash and merge, which would leave the release tag out of dev.
  • A 1.x patch: plan 7.6. The branch is cut from v1.5.0 and needs a one-time bootstrap PR, because 1.5.0's own workflows don't know 1.x.

What the checks enforce

  • Rulesets (after merge):

    • merge commit only into main and beta; squash only into dev, so the one kind of PR that must be a merge commit, a back-merge, needs an admin to widen the ruleset for it;
    • 1 approval everywhere;
    • Commit messages and Promotion source must pass on every protected branch, and the Tests matrix on the release branches;
    • the back-merge's deploy key is the one bypass actor on dev.
  • Promotion source:

    • beta takes PRs only from dev, and only once dev contains main;
    • main takes beta, and only once beta contains main;
    • main also takes a hotfix branch, as long as it carries no commits from dev;
    • 1.x never goes into dev, and no fork targets a release branch.

    A PR's result is for its head, and a tag main gains afterwards does not re-run it, so the release run on beta checks again that beta contains main before it publishes. In both places only exit 1 of git's ancestry test means "not an ancestor"; a git error fails the check with git's own message instead of a wrong remedy.

  • Commit messages:

    • a PR into dev is linted as the squash commit it becomes, title plus description: the title must be conventional, with no exemptions, so GitHub's Revert "…" or Merge … fail;
    • a hotfix's commits must be conventional;
    • a breaking change must carry ! in its header. A BREAKING CHANGE: footer alone, in a commit body or a PR description, is ignored by the release, so the check refuses one under a header without !.

What each merge will publish

These come from replaying the flow against a local copy of the repository with this PR's config:

Step Version
dev → beta, once this PR and a fix are on dev 2.0.0-beta.3
beta → main 2.0.0 on latest
back-merge, then a feat goes dev → beta 2.1.0-beta.1
the same without the back-merge 2.0.0-beta.4; Promotion source refuses this promotion, and the release run would too
hotfix → main 2.0.1; beta then continues at 2.1.0-beta.2
a bootstrapped 1.x, then a fix 1.5.1 on release-1.x

Two things still need a person:

  • a back-merge that conflicts with dev, resolved in a PR that an admin lets through as a merge commit; the next promotion is refused until then, so it cannot be missed;
  • curating 2.0.0's notes once.

Changes

  • release.config.cjs
    • @semantic-release/git and @semantic-release/changelog are gone, and so is the alpha branch.
    • The conventionalcommits preset replaces angular, which ignored !: a feat!: alone released nothing.
    • The parser reads only the commit header: noteKeywords, issuePrefixes and fieldPattern are all null. A squash body is the PR description, where a line like * Breaking change: none forced a major, every #N printed as "closes", and -notes- crashed the notes generator.
    • A revert(scope): header releases a patch. The breaking rule is repeated beside it because a matched custom rule skips the defaults; every matching rule applies and the highest wins.
  • test/release-config.test.ts: runs the analyzer and notes generator with the config's own options, including a promotion that releases from the squash commits it brings and never from its merge commit. With the options emptied, it fails.
  • .github/workflows/pull-requests.yml: Commit messages and Promotion source, as above. A PR into dev is linted as its squash message, title plus description, with .github/commitlint-pull-request.cjs: it exempts no title and turns off the body line-length rules, so the one commitlint rule below decides what a breaking note is. The title path checks out one commit; only the hotfix lint needs history.
  • commitlint.config.cjs: the rule refusing a BREAKING CHANGE: footer without !. It matches the note's title in any case, as the parser finds the keyword, so a lowercase footer is refused, and so is a line such as * Breaking change: none, which the parser reads as a note; the message says to drop it. test/commitlint-config.test.ts runs the CLI over the footer, the ! header, a lowercase footer, that line, a description's bold footer under a title with and without !, a long description, and GitHub's own titles.
  • release.yml: alpha leaves the triggers; a concurrency group per branch, pinned actions, the token declared on the job, a 30-minute timeout on the release job, and a guard on beta that it contains main before anything is installed, with the remedy in the run summary. The back-merge job runs after every push to main whatever the release job's result, since a failed, timed-out or cancelled run may already have pushed the tag. With the deploy key it merges main into dev with a merge commit and pushes; a push that lost a race is retried, and a conflict or a refused push fails at once with the steps in the run summary, the ruleset widening included. Without the key it prints the compare link, so this PR can merge before the key exists. It times out after ten minutes, so a hung push cannot hold the next release behind the concurrency group.
  • test.yml: alpha leaves its pull request triggers.
  • check_build.yml: deleted; Tests already builds on three platforms.
  • .ai/instructions.md: the default branch is dev; a three-sentence Releases section points at the plan's section 8 for promotions, hotfixes and back-merges; one paragraph gives the PR-title conventions. The procedure itself stays in the plan, since every session reads this file in full.

Verification

  • Release flow: replayed with this PR's exact config, with the npm and GitHub plugins swapped for a notes recorder. It gave the versions in the table above. No run pushed a commit to a branch, and the 2.0.0 notes shrink from 337 lines to 60.
  • Workflow scripts: the scripts in pull-requests.yml, read from the YAML, ran against 20 cases, including the ancestry checks against the real origin/main, origin/dev and origin/beta, and a dev that lacks main. The PR lint step ran the same way over a description with a breaking footer under a title with and without !, a long description, CRLF bodies, and a GitHub revert title; over the last 60 PRs merged into dev, 59 pass, and the one refused is feat(portal): build the documentation portal locally with Fumadocs #343, whose description carries a real BREAKING CHANGE: footer under a feat(portal): title. The back-merge job's script ran against a throwaway repository: a clean back-merge, an already-merged dev, a conflict, and a refused push, plus the rejection text git prints for a lost race. The beta guard ran against a checkout that contains main, one that does not, and a repository with no origin/main.
  • Lint and tests: actionlint is clean. The unit suite has 1513 passing and 44 pending, and lint is clean.

After merge (admin, per the plan)

  1. Settings (7.1):
    • squash default message → pull request title and description;
    • untick Allow rebase merging;
    • untick Allow GitHub Actions to create and approve pull requests.
  2. Rulesets (7.3):
    • dev: squash only, 1 approval, required checks Commit messages and Promotion source; bypass: deploy keys, always. A back-merge PR into dev (on a conflict, or before the key exists) is the one time an admin adds merge commit to this ruleset, for that merge only. A PR opened before this merge reports the checks after its next event; editing its title once is enough.
    • release branches (main, beta): merge commit only, 1 approval, and the six Tests jobs, Commit messages and Promotion source as required checks. "Up to date" off; admin bypass for pull requests only.
    • release tags (v*): restrict updates and deletions.
    • Delete no-direct-commits last.
  3. The back-merge key (7.3), only after no-direct-commits is gone: generate an ed25519 key pair; add the public half as a deploy key with write access; create the back-merge environment, limit its deployment branches to main, and add the private half as its secret BACK_MERGE_DEPLOY_KEY; delete both files. Until then the release run prints the back-merge link instead of pushing, and that PR needs the same widening of dev's ruleset.
  4. 2.0.0 (7.4): the @2 schema PR, then dev → beta for 2.0.0-beta.3, then beta → main for 2.0.0. Afterwards chore: retire CHANGELOG.md and the committed version after 2.0.0 #420 retires CHANGELOG.md and the committed version.

Squash this PR into dev as usual: its ci header releases nothing by itself.

🤖 Generated with Claude Code

saeedjamshaid and others added 2 commits September 29, 2026 19:42
The plan for making a merge into main, beta or alpha the whole release:
no release commit, the conventionalcommits preset with a parser that
reads only the commit header, merge-commit promotions, and the rulesets
and settings that go with them. Every version it promises was
reproduced by running semantic-release against a local copy of the
repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A merge into main, beta or alpha is now the whole release. semantic-release
no longer commits CHANGELOG.md and package.json back to the branch, so no
ruleset has to be switched off around a release; the notes live in the
GitHub release.

- The conventionalcommits preset replaces angular, which ignored `!`. Its
  parser reads only the commit header: a squash body is the PR description,
  and a line in it could otherwise force a major release, print "closes"
  links, or crash the notes generator. A revert header releases a patch.
- A test runs the analyzer and notes generator with the config's own
  options, so a preset or parser bump that changes their meaning fails CI.
- A new pull request workflow lints PR titles into dev and hotfix commits
  into main, and holds each release branch to the branches it may take
  pull requests from, beta containing main before it is promoted.
- commitlint refuses a BREAKING CHANGE footer without `!` in the header.
- The release run links the main -> dev back-merge after every push to main.
- check_build.yml goes; Tests already builds on three platforms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After every push to main, the release run merges main into dev with a
merge commit and pushes it with a deploy key kept in the back-merge
environment; without the key it prints the compare link as before.
Promotion source refuses dev -> alpha and dev -> beta until dev contains
main, so a back-merge that did not happen blocks the next prerelease
instead of mis-numbering it.

From the review of the whole PR: the conflict summary keeps its
<name>/merge-main placeholder, a refused push fails at once instead of
being retried as a lost race, the two checks become required on dev too,
the commitlint rule reads the note's title so "Breaking change: none" no
longer trips it, that rule gets a test, and the release job's actions
are pinned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread .github/workflows/release.yml Fixed
saeedjamshaid and others added 3 commits September 30, 2026 15:10
Nothing has shipped from an alpha branch since 1.1.0-alpha.22, and a
second prerelease line costs a promotion PR, a ruleset entry and a gate
case each time. Left in the config, an alpha branch created by accident
would publish on its first push. So release.config.cjs, release.yml,
test.yml and pull-requests.yml stop naming it, and the plan records the
decision as D9.

The release flow replayed without the alpha steps gives the same beta
and main versions as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SonarCloud fails the quality gate on a write permission declared at the
workflow level (S8233). The block moves into the release job, unchanged,
so each job states its own token; the back-merge job already had its
read-only one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A hung push would otherwise hold the run for six hours, and the next
merge into main would wait behind the release concurrency group. The
plan also notes that the 1.x bootstrap PR needs the same job-level
permissions as this one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…k-merge PR

A feature PR merged with a merge commit lands its unlinted commits on dev,
and nothing caught it. dev's ruleset now allows only squash (D10). The
release run's back-merge pushes past it with the deploy key, and the one PR
that must be a merge commit, a back-merge on a conflict or before the key
exists, has an admin widen the ruleset for that merge. The job's two
summaries, the plan and the instructions say so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread package.json
Comment thread .ai/instructions.md
Comment thread test/release-config.test.ts Outdated
Comment on lines +70 to +78
it('releases nothing for a promotion merge commit or a documentation change', async () => {
expect(
await releaseOf(
'Merge pull request #404 from apimatic/dev\n\nRelease',
'docs: add the CONTEXT.md glossary (#365)'
)
).to.be.null;
});
});

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.

A promotion does release, from the squash commits its merge brings in; only the merge commit's own header (Merge pull request #N from apimatic/dev) adds nothing. This test pairs that merge commit with a docs: commit only, so its name reads as "a promotion releases nothing". Showing both sides would say what's meant:

Suggested change
it('releases nothing for a promotion merge commit or a documentation change', async () => {
expect(
await releaseOf(
'Merge pull request #404 from apimatic/dev\n\nRelease',
'docs: add the CONTEXT.md glossary (#365)'
)
).to.be.null;
});
});
it('releases from the commits a promotion brings, never from its merge commit', async () => {
const merge = 'Merge pull request #404 from apimatic/dev\n\nRelease';
expect(await releaseOf(merge, 'fix(portal): keep the header on one line (#398)')).to.equal('patch');
expect(await releaseOf(merge, 'docs: add the CONTEXT.md glossary (#365)')).to.be.null;
});
});

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Taken as suggested (f8693fb).

Comment thread test/commitlint-config.test.ts Outdated
Comment on lines +26 to +28
it('ignores a body line that only mentions breaking changes', () => {
expect(lint('fix(sdk): drop the legacy flag\n\n* Breaking change: none').valid).to.equal(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.

Not a blocker, but the rule's case sensitivity doesn't match the parser's. The parser finds footer keywords case-insensitively (conventional-commits-parser's regex.js:17) and keeps the commit's own spelling as the note title, and the rule then tests that title case-sensitively. Running commitlint on fix(sdk): … with each body line:

Refused Accepted
BREAKING CHANGE: … * Breaking change: none
* BREAKING CHANGE: … breaking change: drops --legacy
| BREAKING CHANGE | yes | Breaking-Change: …
BREAKING CHANGE drops (no colon) BREAKING CHANGES: … (the plural is no keyword at all)

So a lowercase footer that really is breaking passes lint and ships as a patch. Tolerating * Breaking change: none buys little: PR descriptions aren't linted today (see my comment on pull-requests.yml:50), only local commits and the commits of a hotfix PR into main are. I'd make the rule's regex /^BREAKING[ -]CHANGE$/i (see my comment on commitlint.config.cjs:8) and turn this case into a refusal:

  it('refuses a breaking-change footer in any case under a header without !', () => {
    expect(lint('fix(sdk): drop the legacy flag\n\nbreaking change: --legacy is removed').valid).to.equal(false);
  });

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Taken (f8693fb): the rule matches the title in any case, the lowercase footer is a refusal, and the test names the rule it expects. As your table implies, * Breaking change: none is now refused too, since the parser reads it as a note; the rule's message says to drop such a line, and a test pins that.

Comment thread commitlint.config.cjs Outdated
rules: {
// The release reads only a commit's header, so a BREAKING CHANGE footer alone would ship as a minor or a patch.
"breaking-change-in-header": ({ header, notes }) => [
!notes.some((note) => /^BREAKING[ -]CHANGE$/.test(note.title)) || /^\w+(\([^)]*\))?!:/.test(header ?? ""),

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.

As I read it, this is valid when there's no BREAKING CHANGE footer or the header has !, i.e. a footer requires a !. The header regex is needed because the parser adds its own BREAKING CHANGE note for a ! header (conventional-commits-parser's CommitParser.js:229-241), so notes alone can't tell the two apart. Naming the two halves would make it read directly:

        "breaking-change-in-header": ({ header, notes }) => {
          const hasBreakingNote = notes.some((note) => /^BREAKING[ -]CHANGE$/i.test(note.title));
          const headerHasBang = /^\w+(\([^)]*\))?!:/.test(header ?? "");
          return [
            !hasBreakingNote || headerHasBang,
            "mark a breaking change with ! in the header, as in feat(sdk)!: …; a BREAKING CHANGE footer alone is ignored"
          ];
        }

The i flag is the case-sensitivity point from my comment on test/commitlint-config.test.ts.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Taken as suggested (f8693fb), with the message extended to cover the Breaking change: none case from the thread above.

Comment thread .github/workflows/pull-requests.yml Outdated
if: github.base_ref == 'dev'
env:
TITLE: ${{ github.event.pull_request.title }}
run: printf '%s\n' "$TITLE" | pnpm exec commitlint --config .github/commitlint-pr-title.cjs --verbose

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 lints the title only, so a BREAKING CHANGE: footer in the description, which becomes the squash body, is never checked, and the header-only parser drops it. A PR titled feat(sdk): drop v3 generation with BREAKING CHANGE: v3 is gone in its description silently ships as a minor. .ai/instructions.md:137 reads as if commitlint refuses that, but it only does for local commits and hotfix commits.

Either check the description here too:

      - name: check the description for a breaking footer
        if: github.base_ref == 'dev'
        env:
          TITLE: ${{ github.event.pull_request.title }}
          BODY: ${{ github.event.pull_request.body }}
        run: |
          if printf '%s\n' "$BODY" | grep -qE '^BREAKING[ -]CHANGE:' && ! printf '%s\n' "$TITLE" | grep -qE '^\w+(\([^)]*\))?!:'; then
            echo "::error::The description has a BREAKING CHANGE footer but the title has no !, and the release reads only the title."
            exit 1
          fi

or narrow line 137 of the instructions to say a footer in a PR description is ignored.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Taken, one level deeper (f8693fb): the step now lints the title plus the description as the squash message they become, through commitlint with .github/commitlint-pull-request.cjs (no title exempt, the body line-length rules off). So breaking-change-in-header is the one definition of a breaking note, and the * and ** forms the parser reads, which the grep would have missed, are refused too. Over the last 60 PRs merged into dev, 59 pass; the one refused is #343, whose description carries a real BREAKING CHANGE: footer under a feat(portal): title. The instructions now say a footer is ignored by the release and refused by the checks.

Comment thread .github/workflows/release.yml
Comment thread release.config.cjs Outdated
changelogFile: "CHANGELOG.md"
preset: "conventionalcommits",
parserOpts: headerOnly,
// A revert squash lacks git's "This reverts commit" line; a matched custom rule skips the defaults, hence breaking first.

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.

Order doesn't matter here: analyze-commit.js applies every matching rule and keeps the highest release, so "hence breaking first" points at the wrong invariant. What matters is that the breaking rule is in the list at all, because once a custom rule matches, the default rules (their major included) are skipped.

Suggested change
// A revert squash lacks git's "This reverts commit" line; a matched custom rule skips the defaults, hence breaking first.
// A revert squash lacks git's "This reverts commit" line; a matched custom rule skips the defaults, so breaking is repeated here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Taken as suggested (f8693fb); the plan's sentence on rule order is corrected too.

Comment thread .ai/plans/apimatic-config.md Outdated
Comment thread .github/workflows/pull-requests.yml Outdated
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
fetch-depth: 0

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.

Nit: the title path needs no history; only the hotfix path into main runs commitlint --from/--to.

Suggested change
fetch-depth: 0
fetch-depth: ${{ github.base_ref == 'dev' && 1 || 0 }}

(Written this way round because 0 is falsy in expressions, so github.base_ref == 'main' && 0 || 1 would always give 1.) The full pnpm install on every title or description edit is the bigger cost, for a check that only needs commitlint.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Taken as suggested (f8693fb).

Fixes from mrafnadeem-apimatic's review of #409, and from two review passes over the fixes:

- the release run on beta checks that beta contains main before it publishes, with the
  remedy in its summary, because a PR's Promotion source result goes stale once main gains
  a tag
- a PR into dev is linted as its squash message, title plus description, so a BREAKING
  CHANGE footer in the description is refused unless the title carries !, and the commitlint
  rule matches the note title in any case, as the parser does
- the release job times out after 30 minutes, and the back-merge runs whatever its result,
  since a timed-out job counts as cancelled
- Promotion source and the guard treat only exit 1 as not an ancestor
- the release-rule comment, the promotion test, the superseded apimatic-config sentence,
  the instructions cut to what every session needs, fetch-depth 1 on the title path, and
  the version placeholder tracked as #420

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Oct 5, 2026

Copy link
Copy Markdown

@saeedjamshaid
saeedjamshaid merged commit 8204e8f into dev Oct 5, 2026
10 checks passed
@mrafnadeem-apimatic
mrafnadeem-apimatic deleted the saeedjamshaid/release-pipeline branch October 5, 2026 07:09
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 2.0.0-beta.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants