@xemahq/xema 0.45.0 → 0.46.0

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.
Files changed (53) hide show
  1. package/dist/commands/biome.d.ts.map +1 -1
  2. package/dist/commands/biome.js +3 -0
  3. package/dist/commands/biome.js.map +1 -1
  4. package/dist/edition/verify-image-evidence.bundle.mjs +1613 -846
  5. package/dist/edition/verify-image-evidence.bundle.provenance.json +516 -474
  6. package/dist/runtime/biome-publish-image-layers.d.ts +20 -0
  7. package/dist/runtime/biome-publish-image-layers.d.ts.map +1 -0
  8. package/dist/runtime/biome-publish-image-layers.js +64 -0
  9. package/dist/runtime/biome-publish-image-layers.js.map +1 -0
  10. package/dist/runtime/biome-publish.d.ts +4 -0
  11. package/dist/runtime/biome-publish.d.ts.map +1 -1
  12. package/dist/runtime/biome-publish.js +25 -5
  13. package/dist/runtime/biome-publish.js.map +1 -1
  14. package/dist/runtime/config/schema.d.ts +4 -3
  15. package/dist/runtime/config/schema.d.ts.map +1 -1
  16. package/dist/runtime/config/schema.js +8 -2
  17. package/dist/runtime/config/schema.js.map +1 -1
  18. package/dist/runtime/infra/infra-catalog.js +1 -1
  19. package/dist/runtime/installation/contract.d.ts +2 -2
  20. package/dist/runtime/installation/contract.d.ts.map +1 -1
  21. package/dist/runtime/installation/contract.js +1 -1
  22. package/dist/runtime/installation/contract.js.map +1 -1
  23. package/edition/README.md +2 -1
  24. package/edition/assemble-producer-input.mjs +2 -0
  25. package/edition/biome-content.mjs +6 -0
  26. package/edition/build-platform-service-index.mjs +2 -0
  27. package/edition/canonical-repository-url.mjs +12 -0
  28. package/edition/cosign-certificate-constraints.mjs +40 -0
  29. package/edition/customer-edition-verbs.mjs +6 -0
  30. package/edition/develop-coverage-readers.mjs +9 -4
  31. package/edition/develop-coverage-run.mjs +2 -1
  32. package/edition/develop-edition.mjs +20 -0
  33. package/edition/develop-record-ancestry.mjs +121 -0
  34. package/edition/develop-select-records.mjs +365 -0
  35. package/edition/distribution-evidence-set.mjs +15 -0
  36. package/edition/edition-config.mjs +27 -1
  37. package/edition/edition-protocol.mjs +8 -1
  38. package/edition/image-evidence.mjs +2 -12
  39. package/edition/kernel-signer-kind.mjs +23 -0
  40. package/edition/lib.mjs +104 -2
  41. package/edition/oci-evidence-verbs.mjs +7 -1
  42. package/edition/oci-image-source-evidence.mjs +129 -21
  43. package/edition/operation-grant-admission.mjs +98 -4
  44. package/edition/operation-requirements.mjs +118 -4
  45. package/edition/plan-image-evidence-refresh.mjs +21 -4
  46. package/edition/platform-service-class.mjs +23 -0
  47. package/edition/platform-service-ownership.mjs +23 -13
  48. package/edition/produce-distribution-completeness-evidence.mjs +2 -0
  49. package/edition/produce-image-source-evidence.mjs +9 -11
  50. package/edition/producer-evidence-bundle.mjs +43 -1
  51. package/edition/record-refused.mjs +11 -0
  52. package/edition/resolve-distribution.mjs +43 -1
  53. package/package.json +3 -3
@@ -14,6 +14,7 @@ import {
14
14
  import {
15
15
  comparablePlatformService,
16
16
  declaredInstallationClass,
17
+ sourceDeclaredRequirements,
17
18
  sourceOwnedCapabilityDomains,
18
19
  } from './platform-service-class.mjs';
19
20
  import {
@@ -144,6 +145,7 @@ function buildTrustedIndex(sourceAuthority) {
144
145
  tier: declaration.tier,
145
146
  ...declaredInstallationClass(declaration),
146
147
  ...sourceOwnedCapabilityDomains(sourceManifest, `${binding.path} at ${before}`),
148
+ ...sourceDeclaredRequirements(sourceManifest, `${binding.path} at ${before}`),
147
149
  repositoryUrl,
148
150
  repo: binding.repo,
149
151
  path: binding.path,
@@ -33,3 +33,15 @@ export function canonicalPublicHttpsRepositoryUrl(raw, context) {
33
33
  }
34
34
  return parsed.toString();
35
35
  }
36
+
37
+ /**
38
+ * The `owner/name` of a canonical public HTTPS repository URL (`https://host/owner/name[.git]`): exactly two
39
+ * path segments, the optional `.git` suffix dropped. Anything else is refused, naming `context`.
40
+ */
41
+ export function repositoryOwnerName(canonicalUrl, context) {
42
+ const segments = new URL(canonicalUrl).pathname.replace(/\.git$/, '').split('/').filter((segment) => segment.length > 0);
43
+ if (segments.length !== 2) {
44
+ throw new Error(`${context} must name exactly <owner>/<name> on its host; got ${JSON.stringify(canonicalUrl)}`);
45
+ }
46
+ return segments.join('/');
47
+ }
@@ -0,0 +1,40 @@
1
+ // cosign-certificate-constraints — the certificate claims every keyless verification here pins.
2
+ //
3
+ // One signer shape, `{certificateIdentity, oidcIssuer, githubWorkflow: {repository, ref}}`, plus the exact
4
+ // workflow commit the certificate must carry. Every `cosign verify*` of this CLI takes its constraints
5
+ // from here, so no call site can leave one of the five out.
6
+
7
+ const WORKFLOW_SHA = /^[0-9a-f]{40}$/;
8
+
9
+ function requireText(value, what) {
10
+ if (typeof value !== 'string' || value.length === 0) {
11
+ throw new Error(`certificate constraints: ${what} must be a non-empty string; got ${JSON.stringify(value)}`);
12
+ }
13
+ return value;
14
+ }
15
+
16
+ /**
17
+ * @param {{ certificateIdentity: string, oidcIssuer: string, githubWorkflow: { repository: string, ref: string } }} signer
18
+ * @param {string} workflowSha the 40-hex commit the signing workflow ran at
19
+ * @returns {string[]} the cosign flags that pin identity, issuer, workflow repository, ref and sha
20
+ */
21
+ export function certificateConstraintArgs(signer, workflowSha) {
22
+ if (signer === null || typeof signer !== 'object') {
23
+ throw new Error('certificate constraints: the signer must be an object');
24
+ }
25
+ if (typeof workflowSha !== 'string' || !WORKFLOW_SHA.test(workflowSha)) {
26
+ throw new Error(`certificate constraints: the workflow sha must be 40 lowercase hex characters; got ${JSON.stringify(workflowSha)}`);
27
+ }
28
+ return [
29
+ '--certificate-identity',
30
+ requireText(signer.certificateIdentity, 'certificateIdentity'),
31
+ '--certificate-oidc-issuer',
32
+ requireText(signer.oidcIssuer, 'oidcIssuer'),
33
+ '--certificate-github-workflow-repository',
34
+ requireText(signer.githubWorkflow?.repository, 'githubWorkflow.repository'),
35
+ '--certificate-github-workflow-ref',
36
+ requireText(signer.githubWorkflow?.ref, 'githubWorkflow.ref'),
37
+ '--certificate-github-workflow-sha',
38
+ workflowSha,
39
+ ];
40
+ }
@@ -213,6 +213,12 @@ const COMMANDS = Object.freeze({
213
213
  switches: ['--json'],
214
214
  required: ['--pins', '--cosign'],
215
215
  },
216
+ 'select-records': {
217
+ positional: false,
218
+ values: ['--branch', '--registry', '--cosign', '--out', '--at', '--published-pins'],
219
+ switches: [],
220
+ required: ['--branch', '--registry', '--cosign', '--out'],
221
+ },
216
222
  'check-overrides': { positional: false, values: ['--overrides'], switches: [], required: ['--overrides'] },
217
223
  'choose-kernel': { positional: false, values: ['--kernel-pin', '--out'], switches: [], required: ['--out'] },
218
224
  'check-existing': {
@@ -12,7 +12,7 @@ import { promisify } from 'node:util';
12
12
 
13
13
  import { AttestationStatus, DEVELOP_EVIDENCE_CHANNEL } from './develop-coverage-verdict.mjs';
14
14
  import { COSIGN_ENV, policyCandidates, sha256Digest, validatePinnedTrustedRoot, verifySigstoreEvidenceDescriptor } from './image-evidence.mjs';
15
- import { resolveImageSourceEvidenceReferrer } from './oci-image-source-evidence.mjs';
15
+ import { resolveImageSourceEvidenceReferrer, writeInvalidReferrerReport } from './oci-image-source-evidence.mjs';
16
16
  import { BUNDLE_MEDIA_TYPE } from './produce-image-source-evidence.mjs';
17
17
  import { loadRegistryCredentialDeclarations, resolveRegistryCredential, splitImageRef } from './registry-credentials.mjs';
18
18
  import { isNotPublishedYet } from './seal-image-digests.mjs';
@@ -59,7 +59,8 @@ export function parseCosignVerifyOutput(stdout, digest) {
59
59
  });
60
60
  }
61
61
 
62
- function defaultRunCosign(cosignPath, args, env) {
62
+ /** Run cosign once; resolves `{code, stdout, stderr}` for a cosign exit, throws when cosign could not run at all. */
63
+ export function defaultRunCosign(cosignPath, args, env) {
63
64
  return execFileAsync(cosignPath, args, { env, timeout: COSIGN_CALL_TIMEOUT_MS, maxBuffer: COSIGN_MAX_OUTPUT_BYTES }).then(
64
65
  ({ stdout }) => ({ code: 0, stdout, stderr: '' }),
65
66
  (error) => {
@@ -69,7 +70,8 @@ function defaultRunCosign(cosignPath, args, env) {
69
70
  );
70
71
  }
71
72
 
72
- function basicAuthParts(credential) {
73
+ /** The username and password of a resolved basic-auth registry credential. */
74
+ export function basicAuthParts(credential) {
73
75
  const decoded = Buffer.from(credential.basicAuth, 'base64').toString('utf8');
74
76
  const colon = decoded.indexOf(':');
75
77
  return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };
@@ -81,13 +83,14 @@ function basicAuthParts(credential) {
81
83
  * @param {string} request.policyRoot the directory the policy's trusted root is read from (the data root)
82
84
  * @param {object} request.trustedCosign the digest-pinned cosign handle (prepareTrustedCosignBinary)
83
85
  * @param {object} request.schemas kernel distribution schemas
86
+ * @param {object} request.expectedContractAuthority the kernel contract authority the data root pins
84
87
  * @param {string} request.dataRoot declares how each registry is read
85
88
  * @param {object} request.env environment the registry credentials are named in
86
89
  * @param {Function} [request.runCosign] (path, args, env) => { code, stdout, stderr }; replaced by tests
87
90
  * @param {Function} [request.fetchImpl] @param {Function} [request.run] oras runner, replaced by tests
88
91
  * @param {Function} [request.clientFor] (authority) => registry client, replaced by tests
89
92
  */
90
- export function createCoverageReaders({ policy, policyRoot, trustedCosign, schemas, dataRoot, env, runCosign = defaultRunCosign, fetchImpl, run, clientFor: injectedClientFor }) {
93
+ export function createCoverageReaders({ policy, policyRoot, trustedCosign, schemas, expectedContractAuthority, dataRoot, env, runCosign = defaultRunCosign, fetchImpl, run, clientFor: injectedClientFor }) {
91
94
  const cosignPath = assertPreparedExecutable(trustedCosign, 'cosign');
92
95
  const declarations = injectedClientFor === undefined ? loadRegistryCredentialDeclarations(dataRoot) : null;
93
96
  const scratch = mkdtempSync(join(tmpdir(), 'xema-coverage-'));
@@ -141,6 +144,8 @@ export function createCoverageReaders({ policy, policyRoot, trustedCosign, schem
141
144
  imageRepository: subject.imageRef,
142
145
  subjectDigest: digest,
143
146
  schemas,
147
+ expectedContractAuthority,
148
+ onInvalidReferrer: writeInvalidReferrerReport,
144
149
  });
145
150
  if (evidence === null) {return { status: AttestationStatus.None };}
146
151
  if (rows.length === 0) {return { status: AttestationStatus.NoRow };}
@@ -8,6 +8,7 @@ import { collectCoverage, coverageExitCode, renderCoverageTable } from './develo
8
8
  import { DEVELOP_EDITION_STEPS, resolveDevelopSnapshot } from './develop-edition-resolve.mjs';
9
9
  import { loadDeclaredPinnedPolicy } from './evidence-trust-policy.mjs';
10
10
  import { prepareTrustedCosignBinary } from './image-evidence.mjs';
11
+ import { readPinnedContractAuthority } from './produce-image-source-evidence.mjs';
11
12
 
12
13
  const stderrProgress = (line) => process.stderr.write(`${line}\n`);
13
14
 
@@ -34,7 +35,7 @@ export async function runDevelopCoverage({
34
35
  const { schemas, inputs, lock } = await resolveDevelopSnapshot({ id, pins, steps, phase: (name) => progress(`[coverage] ${name}`) });
35
36
  const { imageLock } = await steps.resolveImages(id, { inputs, lock });
36
37
  progress(`[coverage] image content hashes computed (${imageLock.subjects.length} subjects)`);
37
- const readers = createCoverageReaders({ policy, policyRoot: dataRoot, trustedCosign, schemas, dataRoot, env, ...readerOptions });
38
+ const readers = createCoverageReaders({ policy, policyRoot: dataRoot, trustedCosign, schemas, expectedContractAuthority: readPinnedContractAuthority({ dataRoot, schemas }), dataRoot, env, ...readerOptions });
38
39
  try {
39
40
  const report = await collectCoverage({ distributionId: id, subjects: imageLock.subjects, policy, readers, progress });
40
41
  write(json ? `${JSON.stringify(report, null, 2)}\n` : renderCoverageTable(report));
@@ -45,6 +45,22 @@
45
45
  // annotations-missing | annotation-mismatch | not-built). Prints a table grouped by producer repo
46
46
  // and `covered N/M` (--json: the same as data). Exit 0 only when every subject is covered, 1 when
47
47
  // not, 2 on an operational error. Takes the roots `resolve` takes; writes nothing.
48
+ // select-records --branch develop --registry <root> --cosign <absolute-path> --out <file> [--at <ISO-8601 UTC>] [--published-pins <file>]
49
+ // for every develop-graph member, take the newest build record the data root's record workflow signed for
50
+ // the branch (<root>/build-records/<name>, verified offline by digest against the pinned trusted root, as
51
+ // signed by <data root>/.github/workflows/record-build.yaml at refs/heads/<branch>, called by the member at the
52
+ // record's own commit) and write { pins, snapshot, images }, once (wx). Any missing, unsigned, mismatched or
53
+ // unreadable record refuses the whole run and writes nothing: a newer bad record is never skipped for an older
54
+ // one. --at takes the newest record completed at or before that time. Only `develop` has a declared signer; the
55
+ // data root must declare dataRootRepositoryUrl. Takes the roots `resolve` takes (no registry root is implied).
56
+ // "Newest" is the newest COMMIT on develop, not the highest run: EVERY record of the branch is read and cosign-verified,
57
+ // the selected commit must equal or descend from every other record's commit (git merge-base on develop fetched
58
+ // from the member), and among that commit's records the highest run and attempt wins. --published-pins takes a file
59
+ // this verb wrote (only .pins is read): a member's published commit is kept when the records are not ahead of it
60
+ // (its own record must exist), a diverged one is refused. Without the flag the rule applies among the records only;
61
+ // part 07 passes the flag.
62
+ // Residual: anyone who can delete registry tags can remove newer records, and this then pins the newest remaining
63
+ // valid one; the guard is part 07's, deploy-latest's owner refuses a snapshot that goes backwards.
48
64
  // choose-kernel [--kernel-pin <40-hex>] --out <file>
49
65
  // choose the develop kernel commit in the --kernel checkout (full history; its refs/remotes/origin/develop is the develop head):
50
66
  // the newest first-parent commit whose declared edition protocol this CLI supports
@@ -74,6 +90,7 @@ import { runDevelopCoverage } from './develop-coverage-run.mjs';
74
90
  import { CoverageExit } from './develop-coverage.mjs';
75
91
  import { resolveDevelopEdition } from './develop-edition-resolve.mjs';
76
92
  import { assertNoToolingOverride, freezeGraph, freezeSources } from './develop-freeze.mjs';
93
+ import { runSelectRecords } from './develop-select-records.mjs';
77
94
  import { LOCAL_WORKFLOW, assertExistingArtifactMatches, developEditionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
78
95
  import { EditionChannel } from './edition-constants.mjs';
79
96
  import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, kernelRepoDir } from './edition-workspace.mjs';
@@ -162,6 +179,9 @@ async function run(argv, env) {
162
179
  json: options['--json'] === true,
163
180
  env,
164
181
  });
182
+ case 'select-records':
183
+ await runSelectRecords({ options, env });
184
+ return;
165
185
  case 'check-existing': {
166
186
  if (customer) {return runCustomerVerb({ channel: EditionChannel.Develop, command, options, env });}
167
187
  assertExistingArtifactMatches({
@@ -0,0 +1,121 @@
1
+ // develop-record-ancestry — which verified record commit is the newest COMMIT on develop.
2
+ //
3
+ // "Newest" is the commit that equals or descends from every other candidate's commit, never the highest run:
4
+ // a re-run of an old commit carries a higher run number and must lose. History comes from git alone: one bare
5
+ // scratch repository per member, fetched blobless from the member's own URL through the same hold and credential
6
+ // environment freeze uses (`withSourceGitEnv`; no token on argv, none beyond the workspace's). Candidates are
7
+ // compared with each other only, never with a published pin.
8
+ //
9
+ // With a published commit for the member, the records may not take it backwards: behind or equal keeps the published
10
+ // commit (its own record must then exist), ahead selects the records' commit, otherwise the run is refused.
11
+ //
12
+ // Refusals (`RecordRefused`): a commit absent from the fetched develop history, two commits that are not
13
+ // ancestor-related. Operational (plain Error): git cannot be spawned, a fetch fails, or merge-base answers
14
+ // anything but 0 (ancestor) or 1 (not an ancestor).
15
+
16
+ import { spawnSync } from 'node:child_process';
17
+ import { mkdtempSync, rmSync } from 'node:fs';
18
+ import { tmpdir } from 'node:os';
19
+ import { join } from 'node:path';
20
+
21
+ import { withSourceGitEnv } from './git-source-auth.mjs';
22
+ import { RecordRefused } from './record-refused.mjs';
23
+
24
+ const DEVELOP_REF = 'refs/heads/develop';
25
+ // One external call to a git remote; a fetch that exceeds it fails naming the member, never a verdict.
26
+ const GIT_CALL_TIMEOUT_MS = 300_000;
27
+ const GIT_MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
28
+
29
+ export class IncomparableRecordCommitsError extends RecordRefused {
30
+ constructor(repo, first, second) {
31
+ super(repo, `record commits ${first.commit} (${first.tags.join(', ')}) and ${second.commit} (${second.tags.join(', ')}) are not ancestor-related on develop`);
32
+ this.name = 'IncomparableRecordCommitsError';
33
+ }
34
+ }
35
+
36
+ export class RecordCommitNotOnDevelopError extends RecordRefused {
37
+ constructor(repo, commit, tags) {
38
+ super(repo, `record commit ${commit} (${tags.join(', ')}) is not in the fetched develop history`);
39
+ this.name = 'RecordCommitNotOnDevelopError';
40
+ }
41
+ }
42
+
43
+ export class DivergedFromPublishedError extends RecordRefused {
44
+ constructor(repo, selected, published) {
45
+ super(repo, `the newest record commit ${selected} and the published commit ${published} are not ancestor-related on develop`);
46
+ this.name = 'DivergedFromPublishedError';
47
+ }
48
+ }
49
+
50
+ /** The published commit is newer than every record, and none of its own survives among the candidates. */
51
+ export class PublishedRecordMissingError extends RecordRefused {
52
+ constructor(repo, published) {
53
+ super(repo, `no verified build record exists for the published commit ${published}; it is newer than every candidate record`);
54
+ this.name = 'PublishedRecordMissingError';
55
+ }
56
+ }
57
+
58
+ /** `{ status, stdout, stderr }` of one git run; a git that could not be spawned or timed out throws. */
59
+ export function defaultRunGit(args, { cwd, env }) {
60
+ const result = spawnSync('git', args, { cwd, env, encoding: 'utf8', timeout: GIT_CALL_TIMEOUT_MS, maxBuffer: GIT_MAX_OUTPUT_BYTES });
61
+ if (result.error !== undefined || result.status === null) {
62
+ throw new Error(`git ${args[0]} could not run: ${result.error?.message ?? `killed by ${result.signal}`}`);
63
+ }
64
+ return { status: result.status, stdout: result.stdout, stderr: result.stderr };
65
+ }
66
+
67
+ /**
68
+ * @param {object} request
69
+ * @param {{repo: string, repositoryUrl: string}} request.member
70
+ * @param {{commit: string, tag: string}[]} request.candidates verified records' commits with their tags
71
+ * @param {string} [request.published] the commit already published for this member: kept when the records are not ahead of it
72
+ * @param {object} request.workspace the workspace config the source hold reads
73
+ * @param {object} request.env
74
+ * @param {Function} [request.runGit] (args, {cwd, env}) => {status, stdout, stderr}; replaced by tests
75
+ * @returns {string} the selected commit
76
+ */
77
+ export function newestCommitOnDevelop({ member, candidates, published, workspace, env, runGit = defaultRunGit }) {
78
+ const byCommit = new Map();
79
+ for (const { commit, tag } of candidates) {
80
+ byCommit.set(commit, [...(byCommit.get(commit) ?? []), tag]);
81
+ }
82
+ const commits = [...byCommit].map(([commit, tags]) => ({ commit, tags }));
83
+ const scratch = mkdtempSync(join(tmpdir(), 'xema-select-history-'));
84
+ try {
85
+ return withSourceGitEnv({ name: member.repo, url: member.repositoryUrl }, { workspace, env }, (gitEnv) => {
86
+ const git = (args, extraEnv = {}) => runGit(args, { cwd: scratch, env: { ...gitEnv, ...extraEnv } });
87
+ const init = git(['init', '--bare', '--quiet']);
88
+ if (init.status !== 0) {throw new Error(`select-records: ${member.repo}: git init failed: ${init.stderr.trim()}`);}
89
+ const fetched = git(['fetch', '--quiet', '--filter=blob:none', '--no-tags', member.repositoryUrl, `+${DEVELOP_REF}:${DEVELOP_REF}`]);
90
+ if (fetched.status !== 0) {
91
+ throw new Error(`select-records: ${member.repo}: git fetch of develop from ${member.repositoryUrl} failed (exit ${fetched.status}): ${fetched.stderr.trim()}`);
92
+ }
93
+ // 0 = ancestor (or equal), 1 = not; anything else is git failing, never a verdict
94
+ const isAncestor = (older, newer) => {
95
+ const result = git(['merge-base', '--is-ancestor', older, newer]);
96
+ if (result.status === 0) {return true;}
97
+ if (result.status === 1) {return false;}
98
+ throw new Error(`select-records: ${member.repo}: git merge-base ${older} ${newer} failed (exit ${result.status}): ${result.stderr.trim()}`);
99
+ };
100
+ const assertOnDevelop = (commit, tags) => {
101
+ // no lazy fetch: a commit the develop fetch did not bring is not on develop
102
+ const known = git(['cat-file', '-e', `${commit}^{commit}`], { GIT_NO_LAZY_FETCH: '1' });
103
+ if (known.status !== 0 || !isAncestor(commit, DEVELOP_REF)) {throw new RecordCommitNotOnDevelopError(member.repo, commit, tags);}
104
+ };
105
+ for (const { commit, tags } of commits) {assertOnDevelop(commit, tags);}
106
+ let selected = commits[0];
107
+ for (const other of commits.slice(1)) {
108
+ if (isAncestor(other.commit, selected.commit)) {continue;}
109
+ if (!isAncestor(selected.commit, other.commit)) {throw new IncomparableRecordCommitsError(member.repo, selected, other);}
110
+ selected = other;
111
+ }
112
+ if (published === undefined) {return selected.commit;}
113
+ assertOnDevelop(published, ['published pin']);
114
+ if (isAncestor(selected.commit, published)) {return published;} // equal, or the records are behind it
115
+ if (isAncestor(published, selected.commit)) {return selected.commit;}
116
+ throw new DivergedFromPublishedError(member.repo, selected.commit, published);
117
+ });
118
+ } finally {
119
+ rmSync(scratch, { recursive: true, force: true });
120
+ }
121
+ }