Skip to content

PFM-ISSUE-34283 - Consolidate the FE 2.0 release flow into a single workflow - #183

Open
vladimir-arsov wants to merge 14 commits into
masterfrom
improvement/PFM-ISSUE-34283-Consolidate-the-FE-2-0-release-flow-into-a-master
Open

vladimir-arsov wants to merge 14 commits into
masterfrom
improvement/PFM-ISSUE-34283-Consolidate-the-FE-2-0-release-flow-into-a-master

Conversation

@vladimir-arsov

@vladimir-arsov vladimir-arsov commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Replaces the two-run FE 2.0 release (branch push → fe-tag.yml pushes a tag → tag push → fe-release.yml publishes) with one run per push to release/* or manual dispatch:

guard → install-deps → build ∥ e2e → release (publish + promote + tag, one job) → GitHub release

Ticket: PFM-ISSUE-34283

Changes

  • .github/workflows/fe-release-flow.yml (new reusable workflow)
    • inputs FORCE and RUN_E2E
    • a guard job that fails a dispatch on any ref other than the default branch or release/*, and fails while an old release caller (workflows/fe-release.yml / workflows/fe-tag.yml) is still in the tree
    • a release job that publishes, promotes and tags, and unpublishes the version again if the publish, the promote or the tag push fails
    • a github-release job that uses GITHUB_TOKEN, sets the notes range from the previous tag of the line, and marks "Latest" only on the highest version/* tag
  • Artifacts tool gets a RELEASE_FLOW mode. It computes the next version, then checks for affected projects since the previous tag of the line (skipped when FORCE is set or for X.Y.1).
    • Publish before tag: all projects are published under the staging dist-tag release-X.Y-pending, then release-X.Y is moved to the new version, then the tag is pushed. A version/* tag always stands for a complete release, and a failed release leaves release-X.Y on the previous version.
    • The cleanup checks every npm unpublish (it can report success and delete nothing), retries once, and fails the step if the version survives.
    • Outputs: released, tag, previous_tag and is_latest, exposed through the composite action.
    • A forced dispatch on the default branch publishes a snapshot of all projects.
    • The [branch-off] guard still runs first.
  • run-many turns an empty or undefined ref into '', so GHA_REF: '' selects all projects again. This was the original intent: pushes to main and release branches build all projects, PRs build only the affected ones.
  • fe-install-deps.yml loses its job-level cancel-in-progress concurrency. Each caller sets its own concurrency: the release caller is non-cancelling, the PR template cancels superseded runs, and the main template has no group, so every push publishes its snapshot.
  • Caller template fe-release.yml is replaced: push to release/*, plus workflow_dispatch with force, plus secrets: inherit. The fe-tag-pushed.yml template is deleted.
  • README gets an "FE Release Flow" section: migration, triggers and force, secrets, recovery, concurrency.

Behavior notes

  • Every push runs the flow, upmerges included, as in the backend repos. Only [branch-off] commits are never tagged or published.
  • Recovery after a failed publish or tag push is "Re-run failed jobs" (or the next push): the cleanup has unpublished the version, so the re-run releases the same version.
  • Cancelling a running release is not supported: the publisher process outlives the cancel and can leave packages behind. The README describes the recovery.
  • Re-running a successful forced dispatch cuts another patch. The README warns about this.

Not in scope

  • A version/* tag ruleset, a tag-triggered recovery path, and skip-if-exists or resumable publish.
  • Pushing the tag with GITHUB_TOKEN (34282), immutable releases (34284), retiring GIT_USER* (34286).
  • Changes to fe-tag.yml / workflows/fe-release.yml. Unmigrated callers keep the old flow.
  • Migrating consumer repos (pilot PFM-ISSUE-34287).
  • Master only: release lines 25.4–26.4 don't get the new workflow. The first line with it is the next branch-off.

Verification

  • jest: new affected-projects, artifacts-handler-release-flow (including [branch-off] over force), utils tests pass. The only failures are the known Windows path-separator baseline tests, which also fail on master.
  • tsc --noEmit is clean.
  • actionlint is clean on the new and changed workflows and the caller template. A copy with the reusable refs rewritten to local paths is also clean, which checks the nested inputs and secrets.

Live validation (Phase A, 2026-11-02 to 2026-11-03; A6 and A8 retested 2026-11-06)

Run in the fork collaborationFactory/clone-cplace-fe against the test registry cplace-npm-test, with the caller pointing at the test branch test/PFM-ISSUE-34283-release-flow. Procedure and per-case details: clone-cplace-fe-live-tests.md.

Case Scenario Result
A1 push, nothing affected PASS: no release, state unchanged
A2 push with a real change PASS: 26.4.2, 27 packages, tag, GitHub release (latest)
A3 dispatch, force: false PASS: build only, release skipped
A4 forced dispatch, nothing affected PASS: 26.4.3
A5 dispatch on a non-release branch PASS: guard fails, nothing else runs
A6 old caller left in the tree PASS: guard fails "Old release callers found". Retest after the review fixes (62410ac) PASS: an active caller still blocks, a commented-out one does not
A7 [branch-off] tip commit PASS: skipped, also for a forced dispatch
A8 publish fails midway, cleanup, re-run first run FAIL: unpublishing dropped release-26.4 from 26 of 27 packages. Fixed with the staging tag (3de1099); retest PASS: release-26.4 kept, re-run released the same 26.4.4. Retest after the review fixes (62410ac) PASS: the cleanup's tag check (exit code 2, tag not pushed) unpublished all 27, re-run released the same 26.4.7
A9 cancel during publish known limitation: the publisher outlived the cancel and left 3 packages behind. A recovery re-run showed npm unpublish reporting success without deleting; not reproducible locally. An unpublish check with retry was added and reverted in review: the next re-run's cleanup unpublishes again, and the README documents the manual fallback. Recovery by re-run PASS: 26.4.5. Documented in the README
A10 re-run of github-release PASS: no-op when the release exists; recreates a deleted release, no publish
A11 three pushes in quick succession PASS: 26.4.6, the middle pending run cancelled, the third checked since 26.4.6 and released nothing
A12 new older line release/26.3 PASS: 26.3.1, not latest, release-26.4 unchanged
A13 forced dispatch on main (snapshot) skipped by decision: would trigger the fork's main workflows; covered by unit tests

Phase B (tag push with GITHUB_TOKEN) follows in PFM-ISSUE-34282.

🤖 Generated with Claude Code

vladimir-arsov and others added 2 commits October 20, 2026 11:58
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…orkflow

- Add fe-release-flow.yml: guard -> install-deps -> build || e2e -> release (tag + publish) -> GitHub release in one run
- Artifacts tool: RELEASE_FLOW mode with FORCE; affected check since the previous tag of the line; outputs released/tag/previous_tag/is_latest
- run-many: normalize an empty/undefined ref so GHA_REF '' selects all projects
- Remove the cancelling concurrency from fe-install-deps.yml
- Replace the fe-release caller template (push release/* + dispatch force), delete fe-tag-pushed template
- Document the flow in the README

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@Benno42 Benno42 left a comment

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.

Great! Clear flow, and the tests pass locally. Live validation is still open. maybe we can test it before merging.

} else {
// affected since the previous release of the line, not since the pushed range:
// a push whose queued run was cancelled must not be missed
this.base = previousTag;

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.

❓After a failed publish, "Re-run failed jobs" finds nothing affected and ends green without publishing, but design.md and plan.md promise a release. Is this intended, or should the run fail when the latest tag has no published artifact? 🤖

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.

Good catch, confirmed: with the previous tag as base, the dangling tag became the base and the re-run found nothing. Fixed by switching to publish before tag:

  • the tool writes the version to $RUNNER_TEMP before publishing, publishes all projects, then pushes the tag
  • a new cleanup step in the release job (failure() || cancelled()) unpublishes that version via the existing ONLY_DELETE_ARTIFACTS + TAG mode, unless the tag is already on the remote
  • the constructor-ordering bug is fixed so TAG on a release/* branch keeps its own version (needed for the cleanup)

Result: a version/* tag always means a complete release, and "Re-run failed jobs" (or the next push) releases the same version. design.md, plan.md and the README are updated accordingly.

Comment thread .github/workflows/fe-install-deps.yml Resolved
Comment thread tools/scripts/run-many/run-many.ts Resolved
Comment thread .github/workflows/fe-release-flow.yml Resolved
Comment thread README.md Resolved
Comment thread .github/workflows/fe-release-flow.yml Resolved
…, caller concurrency

- Publish first, then tag; a failed or cancelled release is unpublished by a cleanup step (ONLY_DELETE_ARTIFACTS + TAG), so a re-run releases the same version
- Fix the constructor ordering: a TAG on a release branch keeps its own version
- Guard fails while old fe-release/fe-tag callers exist; deprecation warnings in fe-tag.yml and workflows/fe-release.yml; link PFM-ISSUE-34282
- Concurrency moves to the caller templates (fe-pr, fe-main); README migration steps
- Release job: plain npm ci, git identity via env, drop the redundant released output
- calculateNewVersion returns a new Version instead of mutating the tag list
- README recovery and design.md aligned (publish before tag, master only)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Benno42
Benno42 previously approved these changes Oct 28, 2026

@Benno42 Benno42 left a comment

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.

LGTM

@vladimir-arsov vladimir-arsov added PR: Ready to Merge and removed PR: In Review The PR is currently being reviewed labels Oct 30, 2026
vladimir-arsov added a commit that referenced this pull request Nov 2, 2026
Test-only: the live test of PR #183 should exercise build and release,
not the consumer's e2e suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe how the clone-cplace-fe test environment is built and the
agent-executable live test cases for the consolidated release flow
(Phase A), plus the follow-up cases for PFM-ISSUE-34282 (Phase B).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vladimir-arsov and others added 8 commits November 2, 2026 13:53
Preflight P3 compared commit lists, so a commit that only adds a spec
file on the feature branch marked the test branch as stale. Compare the
changed files outside specs/ instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st run

- P2 skips .github/workflow-templates, which keep @master on purpose
- P5 checks unmasked registry paths; the printed .npmrc line is masked
  because JFROG_URL is a secret
- lib.sh: dist-tag query needs -XGET; a missing latest release is
  recorded as "none" instead of the 404 body
- 26.4.1 has 27 @cplace-next tarballs (the 28th match is a backend
  @cplace-assets package)
- document the token permissions the tests need, and the separate
  read-only token for cplace-fe

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…published

Live test A8 showed that unpublishing a version drops every npm dist-tag
pointing to it: after a failed release, 26 of 27 packages had no
release-X.Y dist-tag at all until the next successful release.

Publish the release under the staging tag release-X.Y-pending and move
release-X.Y (promote phase) only after every project is published,
before the version tag is pushed. A failed publish now leaves
release-X.Y on the previous version. A failed promotion fails the run
and is cleaned up like a failed publish.

Also update the live test document: the staging-tag expectations, the
A7 follow-up commit ([branch-off] tip blocks even forced releases), the
long-path clone note, CRLF-safe jq and the job_log helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt cancelling

Live test A9: npm unpublish reported success for cf-devkit@26.4.5 and deleted
nothing. deleteArtifact now checks that the version is gone, rechecks once after
a delay for lagging metadata, retries the unpublish once, and fails the step if
the version survives.

Cancelling a release during the publish is documented as not supported: the
publisher survives the cancel as an orphan and keeps publishing while the
cleanup runs. The README describes the recovery by re-runs, which A9 verified.

The live-test document gets the updated A9 case and reads one run attempt in
capture().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Kaltenbach Kaltenbach added PR: In Review The PR is currently being reviewed and removed PR: Ready to Merge labels Nov 6, 2026
console.log(
`Promote phase: moving ${npmTag} to ${this.calculatedNewVersion}`
);
this.projects.forEach((project) => project.promote());

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.

Promote runs before the tag push, so a failure after promote still drops release-X.Y. The staging tag exists so that a failed run leaves release-X.Y on the previous version. But promote() moves release-X.Y to the new version before git tag/git push. If the push fails (PAT expired, branch protection on tags, network), or the run is cancelled between promote and push, or promote() hits process.exit(1) on project k after projects 1..k-1 were already promoted, the cleanup step sees no remote tag and unpublishes the version. That deletes every dist-tag pointing to it, so release-X.Y disappears for the promoted packages instead of staying on the previous version. Consumers resolving @scope/pkg@release-X.Y then break. Consider pushing the tag first and promoting afterwards (a promote failure after the push then only needs a re-run of promote, not an unpublish), or restoring release-X.Y to previousTag's version in the cleanup path.

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.

Agreed on the analysis: unpublishing drops release-X.Y instead of putting it back on the previous version. In practice our consumers pin exact versions or ranges like ^26.4, and package-lock.json is mandatory, so npm ci installs the locked version without resolving the tag. Only a fresh resolve of the tag (npm update, npm install <pkg>@release-X.Y) fails until the next successful release. Restoring the tag in the cleanup would add an untested error path for a tag nobody resolves, so we documented it in Recovery, including the manual npm dist-tag add fix (62410ac).

Comment thread .github/workflows/fe-release-flow.yml Outdated
exit 0
fi
tag=$(cat "$file")
if git ls-remote --exit-code --tags origin "refs/tags/$tag" > /dev/null; then

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 failing git ls-remote is treated as "tag not pushed", which unpublishes a finished release. git ls-remote --exit-code exits 2 only when the ref is missing; auth or network errors exit 128. The if treats any non-zero exit as "not pushed". So if the release step failed after the tag was pushed (or was cancelled) and the remote check then hits a transient error, the next step unpublishes a version whose tag already exists. The tag then points to a release with no artifacts, and the next run computes the next patch, so that version is lost. Check for exit code 2 explicitly and fail the step on any other code, for example git ls-remote ...; rc=$?; if [ $rc -eq 0 ]; then ...; elif [ $rc -ne 2 ]; then echo '::error::...'; exit 1; fi.

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.

Good catch, fixed in 62410ac. The check now acts on the exit code: 0 means pushed and nothing is cleaned up, 2 means not pushed and the version is unpublished. Any other code fails the step with an ::error:: that names the code, and nothing is unpublished, so a transient auth or network error can no longer remove a finished release.

# a new push to the branch cancels the run of the previous one
concurrency:
group: fe-main-${{ github.ref }}
cancel-in-progress: 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.

cancel-in-progress: true on the main workflow can cancel a snapshot publish mid-run, and the next run never publishes those projects. Before this change only the install-deps job was cancellable. Now a second push to main cancels the whole previous run, including the snapshot job while it publishes. The new run uses GHA_BASE: ${{ github.event.before }}, which is the cancelled push's head. So projects that were affected only by the cancelled push are not affected in the new run, and they get no snapshot until they change again. Some may also be left half-published. The release flow already guards against this ("a push whose queued run was cancelled must not be missed"), but main snapshots do not. Use cancel-in-progress: false here, or keep cancellation scoped to the jobs before snapshot.

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.

Agreed, a main build must not be cancelled. The fe-main template now has no concurrency group (62410ac), so every push runs to completion and publishes its snapshot. A group with cancel-in-progress: false wouldn't be enough: GitHub keeps one pending run per group and drops it when a third push arrives, and the next run's github.event.before base would skip that push's projects. The README Concurrency section explains this.

Comment thread tools/scripts/artifacts/nx-project.ts Outdated
console.warn(
`${scopedPackage}@${version.toString()} is still in the registry after npm unpublish reported success, retrying`
);
this.unpublish(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.

The retry runs npm unpublish again on a version that is probably already gone, and its E404 now fails the job. isStillPublished returns true when registry metadata still lists the version after 5s. That is the stale-metadata case the doc comment describes, and JFrog npm metadata recalculation can take longer than 5s. The second this.unpublish(version) then gets a 404 for the already-deleted version. execSync throws, the outer catch sees status !== 0, and it calls process.exit(1). So a successful deletion now fails fe-pr-close, fe-cleanup-snapshots, PR snapshots (which delete before publishing) and the release cleanup. npm show can also return a cached packument because --prefer-online is missing. Treat E404 from the retry unpublish as success (reuse isMissingFromRegistry), and add --prefer-online to the existence check.

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.

Thanks, valid (this also covers the finding on the 5s sleep). On reflection we removed the check and retry completely in 62410ac. It was added after one observation in live test A9: during the recovery of a deliberately cancelled release job, npm unpublish reported success for one package and deleted nothing. We couldn't reproduce it, and the cause is unknown.

Cancelling a running release job is not supported and shouldn't happen. If it does, the README Recovery section documents the cleanup. A package that survives the cleanup makes the next re-run fail with E403 on that package, and that re-run's cleanup unpublishes the version again; if it still survives, it is unpublished by hand. A check with a retry and a sleep in the shared deleteArtifact path wasn't worth it for this case. deleteArtifact is back to one existence check and one unpublish.


console.log(`Tag phase: pushing ${gitTag}`);
console.log(execSync(`git tag ${gitTag}`).toString());
console.log(execSync(`git push origin ${gitTag}`).toString());

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.

If the run dies after git push but before writeOutput, the GitHub release is never created and nothing retries it. Cancellation or a runner loss in that window means the cleanup sees the remote tag and does nothing, which is correct. But tag is never written, so github-release is skipped. github-release also needs release to succeed, so a later failure in this step skips it as well. The next push computes affected projects since this tag, finds nothing, and returns, so the GitHub release for this tag is never created. Write the outputs, or at least tag, right after the push and before anything else can fail, and consider if: !cancelled() && needs.release.outputs.tag != '' on github-release.

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 window is the single line between git push and writeOutput, and only the step summary follows the output. Dying there takes a cancel, which is not supported for a running release (README), or a runner loss. Neither would be caught by !cancelled(), because the output isn't written in that case. If it does happen, the README covers it: gh release create version/X.Y.Z --verify-tag --generate-notes. So no code change here.

Comment thread tools/scripts/artifacts/nx-project.ts Outdated
version: Version
): Promise<boolean> {
if (!this.packageExists(scopedPackage, version.toString())) return false;
await NxProject.sleep(NxProject.UNPUBLISH_RECHECK_DELAY_MS);

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 5s sleep is added to the shared deleteArtifact path, which runs once per version. deleteArtifact is shared by deleteSnapshots (a loop over every snapshot of every project), fe-pr-close, and PR snapshots. Whenever metadata is briefly stale, each deletion now waits 5s in sequence, so a cleanup run over N snapshots can take N×5s longer. Before this change it did one existence check and one unpublish. Consider running the post-unpublish verification only in the release cleanup (where a lingering version blocks the re-release), or checking all versions once at the end.

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.

Resolved by removing the check completely in 62410ac, see the reply on the retry finding above: it only covered one unexplained case in the unsupported cancel scenario, and deleteArtifact is back to one existence check and one unpublish, without a sleep.

jobs:
install-deps:
runs-on: ubuntu-latest
concurrency:

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.

Removing the job-level concurrency also removes cancellation for every consumer that has not copied the new fe-main/fe-pr templates. Workflow templates are copied into consumer repos by hand, and the new workflow-level concurrency blocks only exist in this repo's templates. Repos still on the old fe-pr.yml lose their only cancel-on-new-push. Every push to a PR then runs a full, uncancelled install, build, code-quality and e2e matrix in parallel with the previous one. Either call out the template update as required in the migration notes, or keep a concurrency group here that the release flow opts out of (for example, an input that sets the group to github.run_id for the release flow).

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.

Intended. A job-level group in fe-install-deps only ever cancels an older run that is still in install-deps, so it only works when the two runs overlap in that window. Concurrency now belongs to the callers, where it always applies. The migration of the consumer repos is announced, and step 3 of the README covers the caller updates: fe-pr adds the cancelling block, and fe-main drops any block.

Comment thread .github/workflows/fe-release-flow.yml Outdated
# an old tag-push caller would publish manually pushed tags and fail on the tags of this flow
- name: Check for old release callers
run: |
old=$(grep -lE 'github-actions/\.github/workflows/(fe-release|fe-tag)\.yml@' .github/workflows/*.y*ml 2> /dev/null || 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.

The old-caller guard matches comments and any other use of fe-tag.yml/fe-release.yml, so it can block every release. grep -lE also matches a commented-out line such as # uses: .../fe-release.yml@master, which is a common way to keep the old caller around during migration. It also matches a workflow that uses fe-release.yml with a non-tag trigger. In both cases the guard fails every release, push or dispatch, until the file is edited. Ignore comment lines (for example grep -vE '^\s*#' before matching), and only flag files that also have a tag-push trigger.

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.

Agreed on comments: since 62410ac the guard ignores lines where the reference follows a #, so a commented-out caller no longer blocks releases. I kept it for every trigger, though. The old fe-tag.yml caller runs on branch pushes, not tag pushes, and would tag in parallel with the new flow. A workflows/fe-release.yml caller with a dispatch or other trigger would also publish. Any active caller of either is a conflict, and the error message says what to do.

return Promise.resolve();
}
if (
if (this.releaseFlow) {

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 forced dispatch on a branch whose tip is a [branch-off] commit succeeds silently and publishes nothing. The branch-off check above returns before tagAndPublishRelease, regardless of FORCE. A maintainer who dispatches with force: true on a fresh release/X.Y, or on the default branch right after branch-off, gets a green run with no tag, no publish and no step summary explaining why. Either let force bypass the branch-off check in the release flow, or write a step summary or warning such as "FORCE ignored: tip is a branch-off commit".

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 skip is intended: branch-off already released what the [branch-off] commit contains, so force must not release it again (covered by a test and live test A7b). The silence was a fair point: since 62410ac a forced dispatch on such a tip prints ::warning::FORCE is ignored: … and writes it to the step summary. The README mentions it under Triggers.

allTags
);
const previousTag = previous.isValid() ? previous.getGitTag() : '';
this.calculatedNewVersion = Utils.calculateNewVersion(

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.

Simplification: calculateNewVersion looks up the same latest tag that was just computed as previous. previous is getLatestTagForReleaseBranch(branch, versionOfBranch, allTags), and calculateNewVersion immediately calls the same function with the same arguments. The new version can be derived directly: previous.isValid() ? previous+1 : branchVersion.1. Alternatively, have calculateNewVersion return {previous, next}. That removes the duplicate lookup and the risk of the two diverging.

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.

Done in 62410ac: Utils.nextVersion(branch, previous) computes the next version from the already computed previous tag. The release flow uses it directly, and calculateNewVersion (still used by the old bump mode) delegates to it.

@Kaltenbach Kaltenbach left a comment

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 the see review findings above

vladimir-arsov and others added 2 commits November 6, 2026 16:36
- Incomplete-release check: act on the git ls-remote exit code. 0 = pushed,
  2 = not pushed (unpublish), anything else fails the step without
  unpublishing, so a transient error cannot remove a finished release.
- Old-caller guard: ignore commented-out references; any active caller of
  fe-release.yml or fe-tag.yml still blocks the release.
- [branch-off] tip with FORCE: still skipped, now with a warning and a step
  summary line.
- Utils.nextVersion derives the next version from the previous tag the
  release flow already looked up; calculateNewVersion delegates to it.
- fe-main template: no concurrency group, so every push publishes its
  snapshot. The README migration step and Concurrency section say why.
- Revert the unpublish check and retry of 37dc554. It covered one
  unexplained case in the unsupported cancel scenario; a leftover package is
  removed by the next re-run's cleanup, as documented in Recovery.
- README Recovery: a failed promote or tag push leaves release-X.Y missing
  until the next release; npm ci and lock-file installs are unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR: In Review The PR is currently being reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants