mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(release): accept retained v1 validation evidence (#159440)
* fix(release): read retained v1 candidate evidence * fix(release): type retained candidate bindings * fix(release): type archived candidate bindings
This commit is contained in:
parent
4a2115c767
commit
d7c378bf4b
5 changed files with 83 additions and 15 deletions
|
|
@ -22,6 +22,12 @@ export interface FullReleaseCandidateRequest {
|
|||
upgradeSurvivorScenarios: string[];
|
||||
}
|
||||
|
||||
export type RecordedFullReleaseCandidateRequest =
|
||||
| FullReleaseCandidateRequest
|
||||
| (Omit<FullReleaseCandidateRequest, "packagePublished" | "schema"> & {
|
||||
schema: "openclaw.full-release-candidate-request/v1";
|
||||
});
|
||||
|
||||
export interface FullReleaseCandidateArtifactIdentity {
|
||||
digest: string;
|
||||
expiresAt: string;
|
||||
|
|
@ -88,18 +94,27 @@ export interface FullReleaseCandidateBinding extends Omit<FullReleaseCandidateMa
|
|||
schema: "openclaw.full-release-candidate-binding/v2";
|
||||
}
|
||||
|
||||
export interface RecordedFullReleaseCandidateBinding extends Omit<
|
||||
FullReleaseCandidateBinding,
|
||||
"request"
|
||||
> {
|
||||
request: RecordedFullReleaseCandidateRequest;
|
||||
}
|
||||
|
||||
export function buildFullReleaseCandidateRequest(
|
||||
input: FullReleaseCandidateRecord,
|
||||
): FullReleaseCandidateRequest;
|
||||
export function buildFullReleaseCandidateBinding(input: {
|
||||
artifact: FullReleaseCandidateRecord;
|
||||
manifest: FullReleaseCandidateRecord;
|
||||
}): FullReleaseCandidateBinding;
|
||||
}): RecordedFullReleaseCandidateBinding;
|
||||
export function canonicalFullReleaseCandidateRequestJson(value: unknown): string;
|
||||
export function candidateRequestSha256(value: unknown): string;
|
||||
export function fullReleaseCandidateArtifactName(requestSha256: string): string;
|
||||
export function validateFullReleaseCandidateRequest(value: unknown): FullReleaseCandidateRequest;
|
||||
export function validateRecordedFullReleaseCandidateRequest(
|
||||
value: unknown,
|
||||
): FullReleaseCandidateRequest;
|
||||
export function validateFullReleaseCandidateBinding(value: unknown): FullReleaseCandidateBinding;
|
||||
): RecordedFullReleaseCandidateRequest;
|
||||
export function validateFullReleaseCandidateBinding(
|
||||
value: unknown,
|
||||
): RecordedFullReleaseCandidateBinding;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import {
|
|||
parseUpgradeSurvivorScenarios,
|
||||
} from "./lib/upgrade-survivor-policy.mjs";
|
||||
|
||||
// Retained evidence must keep its producer schema so its immutable request and
|
||||
// manifest digests remain reproducible. Fresh candidate requests stay v2-only.
|
||||
const RETAINED_FULL_RELEASE_CANDIDATE_REQUEST_SCHEMA = "openclaw.full-release-candidate-request/v1";
|
||||
const FULL_RELEASE_CANDIDATE_REQUEST_SCHEMA = "openclaw.full-release-candidate-request/v2";
|
||||
const FULL_RELEASE_CANDIDATE_MANIFEST_SCHEMA = "openclaw.full-release-candidate/v2";
|
||||
const FULL_RELEASE_CANDIDATE_BINDING_SCHEMA = "openclaw.full-release-candidate-binding/v2";
|
||||
|
|
@ -161,18 +164,31 @@ export function buildFullReleaseCandidateRequest(input) {
|
|||
|
||||
export function validateFullReleaseCandidateRequest(value) {
|
||||
const request = validateRecordedFullReleaseCandidateRequest(value);
|
||||
if (request.schema !== FULL_RELEASE_CANDIDATE_REQUEST_SCHEMA) {
|
||||
fail("full release candidate request schema is invalid");
|
||||
}
|
||||
parseUpgradeSurvivorScenarios(request.upgradeSurvivorScenarios.join(" "));
|
||||
return request;
|
||||
}
|
||||
|
||||
export function validateRecordedFullReleaseCandidateRequest(value) {
|
||||
if (!isRecord(value)) {
|
||||
fail("full release candidate request must be an object");
|
||||
}
|
||||
const schema = value.schema;
|
||||
if (
|
||||
schema !== RETAINED_FULL_RELEASE_CANDIDATE_REQUEST_SCHEMA &&
|
||||
schema !== FULL_RELEASE_CANDIDATE_REQUEST_SCHEMA
|
||||
) {
|
||||
fail("full release candidate request schema is invalid");
|
||||
}
|
||||
exactKeys(
|
||||
value,
|
||||
[
|
||||
"allowFrozenTargetScenarioOmissions",
|
||||
"allowUnreleasedChangelog",
|
||||
"contractVersions",
|
||||
"packagePublished",
|
||||
...(schema === FULL_RELEASE_CANDIDATE_REQUEST_SCHEMA ? ["packagePublished"] : []),
|
||||
"releaseProfile",
|
||||
"releaseSoak",
|
||||
"repository",
|
||||
|
|
@ -244,20 +260,21 @@ export function validateRecordedFullReleaseCandidateRequest(value) {
|
|||
) {
|
||||
fail("full release candidate request upgradeSurvivorScenarios are not normalized");
|
||||
}
|
||||
if (value.schema !== FULL_RELEASE_CANDIDATE_REQUEST_SCHEMA) {
|
||||
fail("full release candidate request schema is invalid");
|
||||
}
|
||||
return {
|
||||
schema: value.schema,
|
||||
schema,
|
||||
repository: repository(value.repository, "full release candidate request repository"),
|
||||
targetSha: sha(value.targetSha, "full release candidate request targetSha"),
|
||||
toolingSha: sha(value.toolingSha, "full release candidate request toolingSha"),
|
||||
releaseProfile,
|
||||
releaseSoak: boolean(value.releaseSoak, "full release candidate request releaseSoak"),
|
||||
packagePublished: boolean(
|
||||
value.packagePublished,
|
||||
"full release candidate request packagePublished",
|
||||
),
|
||||
...(schema === FULL_RELEASE_CANDIDATE_REQUEST_SCHEMA
|
||||
? {
|
||||
packagePublished: boolean(
|
||||
value.packagePublished,
|
||||
"full release candidate request packagePublished",
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
upgradeSurvivorBaselines: baselines,
|
||||
upgradeSurvivorScenarios: scenarios,
|
||||
allowFrozenTargetScenarioOmissions: boolean(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type {
|
||||
FullReleaseCandidateBinding,
|
||||
FullReleaseCandidateRequest,
|
||||
RecordedFullReleaseCandidateBinding,
|
||||
} from "../full-release-candidate-contract.mjs";
|
||||
|
||||
export class CandidateConstituentUnavailableError extends Error {}
|
||||
|
|
@ -42,7 +43,7 @@ export function validateCandidateBinding(
|
|||
now?: number;
|
||||
request?: FullReleaseCandidateRequest;
|
||||
},
|
||||
): FullReleaseCandidateBinding;
|
||||
): RecordedFullReleaseCandidateBinding;
|
||||
|
||||
export function resolveCandidateBinding(input: {
|
||||
freshBinding?: unknown;
|
||||
|
|
@ -63,4 +64,4 @@ export function verifySealedFullReleaseCandidate(input: {
|
|||
readRunAttempt: (runId: string, runAttempt: string) => Promise<unknown>;
|
||||
readWorkflowJobs: (runId: string, runAttempt: string) => Promise<unknown>;
|
||||
token: string;
|
||||
}): Promise<FullReleaseCandidateBinding>;
|
||||
}): Promise<RecordedFullReleaseCandidateBinding>;
|
||||
|
|
|
|||
|
|
@ -310,6 +310,9 @@ export function validateCandidateBinding(
|
|||
|
||||
export function candidateArtifactJsonFromBinding(value) {
|
||||
const binding = validateFullReleaseCandidateBinding(value);
|
||||
if (binding.request.packagePublished === undefined) {
|
||||
fail("retained v1 candidate evidence cannot supply package provenance");
|
||||
}
|
||||
return JSON.stringify({
|
||||
packagePublished: binding.request.packagePublished,
|
||||
packageArtifactName: binding.package.artifact.name,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ import {
|
|||
validateFullReleaseCandidateBinding,
|
||||
validateFullReleaseCandidateRequest,
|
||||
} from "../../scripts/full-release-candidate-contract.mjs";
|
||||
import { resolveCandidateBinding } from "../../scripts/lib/full-release-candidate-reuse.mjs";
|
||||
import {
|
||||
candidateArtifactJsonFromBinding,
|
||||
resolveCandidateBinding,
|
||||
} from "../../scripts/lib/full-release-candidate-reuse.mjs";
|
||||
import {
|
||||
canonicalTestJson,
|
||||
canonicalTestSha256,
|
||||
|
|
@ -138,6 +141,35 @@ describe("full release candidate contract", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("validates retained v1 request and manifest digests without rewriting them", () => {
|
||||
const currentManifest = fullReleaseCandidateManifestFixture();
|
||||
const { packagePublished: _packagePublished, ...requestFields } = currentManifest.request;
|
||||
const request = {
|
||||
...requestFields,
|
||||
schema: "openclaw.full-release-candidate-request/v1" as const,
|
||||
};
|
||||
const requestSha256 = canonicalTestSha256(request);
|
||||
const retainedManifest = { ...currentManifest, request, requestSha256 };
|
||||
const currentBinding = fullReleaseCandidateBindingFixture();
|
||||
const binding = {
|
||||
...currentBinding,
|
||||
request,
|
||||
requestSha256,
|
||||
evidenceArtifact: {
|
||||
...currentBinding.evidenceArtifact,
|
||||
name: `full-release-candidate-v2-${requestSha256}`,
|
||||
},
|
||||
manifestSha256: canonicalTestSha256(retainedManifest),
|
||||
};
|
||||
|
||||
expect(validateFullReleaseCandidateBinding(binding)).toEqual(binding);
|
||||
expect(candidateRequestSha256(request)).toBe(requestSha256);
|
||||
expect(() => validateFullReleaseCandidateRequest(request)).toThrow("schema is invalid");
|
||||
expect(() => candidateArtifactJsonFromBinding(binding)).toThrow(
|
||||
"retained v1 candidate evidence cannot supply package provenance",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["repository", { repository: "openclaw/fork" }],
|
||||
["target SHA", { targetSha: "4".repeat(40) }],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue