rollbridge 0.1.47 → 0.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rollbridge",
3
- "version": "0.1.47",
3
+ "version": "0.1.48",
4
4
  "description": "Zero-downtime process supervisor and local traffic switcher for deploy-managed apps.",
5
5
  "keywords": [
6
6
  "deploy",
package/src/daemon.js CHANGED
@@ -1895,7 +1895,7 @@ export default class RollbridgeDaemon {
1895
1895
  if (transition.previousReleaseId !== identity.previousReleaseId) {
1896
1896
  throw new Error(`Generation transition previous release is ${transition.previousReleaseId}; refusing stale recovery for ${identity.previousReleaseId}`)
1897
1897
  }
1898
- if (transition.phase !== "activating_candidate" && transition.phase !== "restoring_previous" && transition.phase !== "retiring_failed_candidate" && transition.phase !== "degraded_active") {
1898
+ if (transition.phase !== "activating_candidate" && transition.phase !== "retiring_previous" && transition.phase !== "restoring_previous" && transition.phase !== "retiring_failed_candidate" && transition.phase !== "degraded_active") {
1899
1899
  throw new Error(`Generation transition ${transition.candidateReleaseId} at ${transition.phase} is not a safe failed pre-commit transition`)
1900
1900
  }
1901
1901
  if (transition.phase === "degraded_active" && !identity.acceptRetiredIncumbent) {
@@ -1923,8 +1923,10 @@ export default class RollbridgeDaemon {
1923
1923
  * @returns {Promise<Record<string, JsonValue>>} Explicit degraded recovery result.
1924
1924
  */
1925
1925
  async acceptRetiredIncumbentTransition(transition) {
1926
- if (transition.phase !== "restoring_previous" && transition.phase !== "degraded_active") throw new Error(`Retired-incumbent recovery requires exactly restoring_previous; transition ${transition.candidateReleaseId} is at ${transition.phase}`)
1927
- if (!terminalRetirementFailure(transition.compensationError)) throw new Error("Retired-incumbent recovery requires a recorded restoration failure compatible with terminal retirement")
1926
+ if (transition.phase !== "retiring_previous" && transition.phase !== "restoring_previous" && transition.phase !== "degraded_active") throw new Error(`Retired-incumbent recovery requires retiring_previous or restoring_previous; transition ${transition.candidateReleaseId} is at ${transition.phase}`)
1927
+ const retirementFailure = transition.phase === "retiring_previous" ? transition.error : transition.compensationError
1928
+
1929
+ if (!terminalRetirementFailure(retirementFailure)) throw new Error("Retired-incumbent recovery requires a recorded restoration failure compatible with terminal retirement")
1928
1930
  if (!transition.previousReleaseId) throw new Error("Retired-incumbent recovery requires a retained previous release")
1929
1931
  const candidate = this.releases.get(transition.candidateReleaseId)
1930
1932
  const previous = this.releases.get(transition.previousReleaseId)
@@ -1958,6 +1960,7 @@ export default class RollbridgeDaemon {
1958
1960
 
1959
1961
  if (transition.phase === "degraded_active") return result
1960
1962
  const previousError = transition.error
1963
+ const previousPhase = transition.phase
1961
1964
  const previousJournalRevision = transition.journalRevision
1962
1965
  const previousUpdatedAt = transition.updatedAt
1963
1966
 
@@ -1968,7 +1971,7 @@ export default class RollbridgeDaemon {
1968
1971
  try {
1969
1972
  await this.checkpointGenerationTransition()
1970
1973
  } catch (error) {
1971
- transition.phase = "restoring_previous"
1974
+ transition.phase = previousPhase
1972
1975
  transition.error = previousError
1973
1976
  transition.journalRevision = previousJournalRevision
1974
1977
  transition.updatedAt = previousUpdatedAt
@@ -2962,7 +2965,8 @@ export function ownerReplacementTransitionAuthorityMatches(transfer, transition,
2962
2965
  function terminalRetirementFailure(failure) {
2963
2966
  return typeof failure === "string" && (
2964
2967
  /^Cannot activate .+ generation from retired$/iu.test(failure) ||
2965
- /^activate command exited non-zero with status \d+$/u.test(failure)
2968
+ /^activate command exited non-zero with status \d+$/u.test(failure) ||
2969
+ /^Release .+ retirement quiescence failed: quiet command exited non-zero with status \d+$/u.test(failure)
2966
2970
  )
2967
2971
  }
2968
2972
 
@@ -799,7 +799,7 @@ test("explicit recovery stops the exact failed candidate and fences degraded inc
799
799
  await assert.rejects(() => exactRecovery({previousReleaseId: "wrong-v1"}), /refusing stale recovery/i)
800
800
  await assert.rejects(() => exactRecovery({revision: "wrong-v2"}), /exact same release, path, revision, and config authority/i)
801
801
  transition.phase = "retiring_failed_candidate"
802
- await assert.rejects(() => exactRecovery(), /exactly restoring_previous/i)
802
+ await assert.rejects(() => exactRecovery(), /requires retiring_previous or restoring_previous/i)
803
803
  transition.phase = "restoring_previous"
804
804
  const terminalFailure = transition.compensationError
805
805
 
@@ -826,6 +826,15 @@ test("explicit recovery stops the exact failed candidate and fences degraded inc
826
826
  const persisted = /** @type {{generationTransition?: import("../src/json.js").JsonValue} | undefined} */ (await readState(fixture.statePath))
827
827
 
828
828
  assert.equal(/** @type {{phase?: string} | undefined} */ (persisted?.generationTransition)?.phase, "degraded_active")
829
+
830
+ transition.phase = "retiring_previous"
831
+ transition.error = "Release v1 retirement quiescence failed: quiet command exited non-zero with status 1"
832
+ transition.compensationError = undefined
833
+ await daemon.checkpointGenerationTransition()
834
+ const legacyRecovery = await exactRecovery()
835
+
836
+ assert.equal(legacyRecovery.recoveryStatus, "retired_incumbent_accepted")
837
+ assert.equal(daemon.status().generationTransition?.phase, "degraded_active", "guarded recovery migrates a legacy terminal retirement fence")
829
838
  await assert.rejects(() => daemon.deploy({releaseId: "bad-v3", releasePath: fixture.root, revision: "bad-v3"}), /health check failed/i)
830
839
  assert.equal(daemon.status().generationTransition?.phase, "degraded_active")
831
840
  assert.equal(statusRelease(daemon, "v1").processes.find(({id}) => id === "web")?.pid, incumbentWebPid)