@ankhorage/devtools 1.14.7 → 1.15.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 (58) hide show
  1. package/README.md +159 -2
  2. package/dist/apmRelease.d.ts +4 -0
  3. package/dist/apmRelease.js +3 -0
  4. package/dist/cli/bin/apm-release.d.ts +2 -0
  5. package/dist/cli/bin/apm-release.js +15 -0
  6. package/dist/cli/commands/apm/sync.d.ts +5 -0
  7. package/dist/cli/commands/apm/sync.js +5 -0
  8. package/dist/cli/commands/apm/validate.d.ts +5 -0
  9. package/dist/cli/commands/apm/validate.js +5 -0
  10. package/dist/cli/commands.d.ts +6 -2
  11. package/dist/cli/commands.js +15 -1
  12. package/dist/cli/index.d.ts +2 -2
  13. package/dist/cli/index.js +9 -0
  14. package/dist/cli/runProviderCommand.js +5 -0
  15. package/dist/features/apm-release-validation/adapters/inbound/runApmReleaseCommandAsync.d.ts +10 -0
  16. package/dist/features/apm-release-validation/adapters/inbound/runApmReleaseCommandAsync.js +51 -0
  17. package/dist/features/apm-release-validation/adapters/outbound/createApmReleaseDescriptorPort.d.ts +3 -0
  18. package/dist/features/apm-release-validation/adapters/outbound/createApmReleaseDescriptorPort.js +31 -0
  19. package/dist/features/apm-release-validation/adapters/outbound/inspectPackedApmReleaseCandidateAsync.d.ts +3 -0
  20. package/dist/features/apm-release-validation/adapters/outbound/inspectPackedApmReleaseCandidateAsync.js +84 -0
  21. package/dist/features/apm-release-validation/adapters/outbound/probePackedApmExtensionAsync.d.ts +11 -0
  22. package/dist/features/apm-release-validation/adapters/outbound/probePackedApmExtensionAsync.js +82 -0
  23. package/dist/features/apm-release-validation/adapters/outbound/readApmReleaseManifestAsync.d.ts +2 -0
  24. package/dist/features/apm-release-validation/adapters/outbound/readApmReleaseManifestAsync.js +9 -0
  25. package/dist/features/apm-release-validation/adapters/outbound/resolveApmReleaseCommandAsync.d.ts +2 -0
  26. package/dist/features/apm-release-validation/adapters/outbound/resolveApmReleaseCommandAsync.js +18 -0
  27. package/dist/features/apm-release-validation/application/synchronizeApmReleaseMetadataAsync.d.ts +3 -0
  28. package/dist/features/apm-release-validation/application/synchronizeApmReleaseMetadataAsync.js +29 -0
  29. package/dist/features/apm-release-validation/application/validateApmReleaseCandidate.d.ts +11 -0
  30. package/dist/features/apm-release-validation/application/validateApmReleaseCandidate.js +76 -0
  31. package/dist/features/apm-release-validation/composition/synchronizeApmReleaseDescriptorAsync.d.ts +2 -0
  32. package/dist/features/apm-release-validation/composition/synchronizeApmReleaseDescriptorAsync.js +6 -0
  33. package/dist/features/apm-release-validation/composition/validatePackedApmReleaseAsync.d.ts +12 -0
  34. package/dist/features/apm-release-validation/composition/validatePackedApmReleaseAsync.js +14 -0
  35. package/dist/features/apm-release-validation/utils/readApmReleaseMetadata.d.ts +2 -0
  36. package/dist/features/apm-release-validation/utils/readApmReleaseMetadata.js +6 -0
  37. package/dist/internal/readmeDocs.js +1 -1
  38. package/dist/owner/synchronizeRenovateOwnerAsync.js +2 -23
  39. package/dist/policy/renderBunPolicyDocumentation.d.ts +3 -0
  40. package/dist/policy/renderBunPolicyDocumentation.js +24 -0
  41. package/dist/tools/eslint/index.js +2 -2
  42. package/dist/tools/eslint/resolveEslintProfile.d.ts +3 -0
  43. package/dist/tools/eslint/{profile.js → resolveEslintProfile.js} +8 -18
  44. package/dist/tools/eslint/resolveEslintProfileFromDetectionInput.d.ts +4 -0
  45. package/dist/tools/eslint/resolveEslintProfileFromDetectionInput.js +10 -0
  46. package/dist/tools/workflows/files/ci.yml +7 -0
  47. package/dist/tools/workflows/files/release.yml +27 -1
  48. package/dist/tools/workflows/index.js +4 -1
  49. package/dist/tools/workflows/renderWorkflowAsync.d.ts +1 -0
  50. package/dist/tools/workflows/renderWorkflowAsync.js +1 -0
  51. package/dist/types/apm-release-validation.d.ts +33 -0
  52. package/dist/types/apm-release-validation.js +1 -0
  53. package/dist/types/bunPolicy.d.ts +5 -0
  54. package/dist/types/bunPolicy.js +1 -0
  55. package/dist/types/public.d.ts +1 -0
  56. package/dist/types/public.js +1 -0
  57. package/package.json +20 -5
  58. package/dist/tools/eslint/profile.d.ts +0 -4
@@ -0,0 +1,9 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { isRecord } from '@ankhorage/utility/object';
3
+ /*** Read a release-owned JSON object while rejecting malformed documents. */
4
+ export async function readApmReleaseManifestAsync(filePath) {
5
+ const value = JSON.parse(await readFile(filePath, 'utf8'));
6
+ if (!isRecord(value))
7
+ throw new Error('APM release document must contain a JSON object.');
8
+ return value;
9
+ }
@@ -0,0 +1,2 @@
1
+ /*** Use Devtools' own built CLI for self-validation and the installed dependency binary in consumers. */
2
+ export declare function resolveApmReleaseCommandAsync(targetDirectory: string): Promise<string>;
@@ -0,0 +1,18 @@
1
+ import { resolve } from 'node:path';
2
+ import { readApmReleaseManifestAsync } from './readApmReleaseManifestAsync.js';
3
+ /*** Use Devtools' own built CLI for self-validation and the installed dependency binary in consumers. */
4
+ export async function resolveApmReleaseCommandAsync(targetDirectory) {
5
+ try {
6
+ const manifest = await readApmReleaseManifestAsync(resolve(targetDirectory, 'package.json'));
7
+ return manifest.name === '@ankhorage/devtools'
8
+ ? 'node ./dist/cli/bin/apm-release.js'
9
+ : CONSUMER_COMMAND;
10
+ }
11
+ catch (error) {
12
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
13
+ return CONSUMER_COMMAND;
14
+ }
15
+ throw error;
16
+ }
17
+ }
18
+ const CONSUMER_COMMAND = './node_modules/.bin/ankhorage-apm-release';
@@ -0,0 +1,3 @@
1
+ import type { ApmReleaseDescriptorPort } from '../../../types/apm-release-validation.js';
2
+ /*** Bind valid owner metadata to the selected candidate version; never silently skip malformed opt-in. */
3
+ export declare function synchronizeApmReleaseMetadataAsync(port: ApmReleaseDescriptorPort): Promise<boolean>;
@@ -0,0 +1,29 @@
1
+ import { validatePackageUpdateMetadata, validateUpdateDescriptor } from '@ankhorage/apm';
2
+ import { isRecord } from '@ankhorage/utility/object';
3
+ import { readApmReleaseMetadata } from '../utils/readApmReleaseMetadata.js';
4
+ /*** Bind valid owner metadata to the selected candidate version; never silently skip malformed opt-in. */
5
+ export async function synchronizeApmReleaseMetadataAsync(port) {
6
+ const manifest = await port.readPackageAsync();
7
+ const metadata = readApmReleaseMetadata(manifest);
8
+ if (metadata === undefined)
9
+ return false;
10
+ const discovery = validatePackageUpdateMetadata(metadata);
11
+ if (!discovery.valid || discovery.metadata === undefined)
12
+ throw new Error('Invalid APM discovery metadata.');
13
+ const descriptor = await port.readDescriptorAsync(discovery.metadata.descriptor);
14
+ const { name, version } = manifest;
15
+ if (typeof name !== 'string' ||
16
+ typeof version !== 'string' ||
17
+ !isRecord(descriptor.owner) ||
18
+ descriptor.owner.name !== name) {
19
+ throw new Error('APM release descriptor must belong to the package being versioned.');
20
+ }
21
+ const synchronized = { ...descriptor, owner: { ...descriptor.owner, version } };
22
+ const validation = validateUpdateDescriptor({
23
+ descriptor: synchronized,
24
+ expectedOwner: { name, version },
25
+ });
26
+ if (!validation.valid)
27
+ throw new Error(validation.blockers.map(({ code, reason }) => `${code}: ${reason}`).join('\n'));
28
+ return port.writeDescriptorAsync(discovery.metadata.descriptor, synchronized);
29
+ }
@@ -0,0 +1,11 @@
1
+ import type { ApmPackedReleaseCandidate, ApmReleaseValidationOptions, ApmReleaseValidationResult } from '../../../types/apm-release-validation.js';
2
+ /*** Validate packed owner evidence using the released APM protocol, never a copied schema.
3
+ * Opt-in packages declare ankh.apm with protocolVersion and a package-relative descriptor.
4
+ * No metadata means not applicable, not a claim that no migration is needed. The descriptor
5
+ * declares supported no-migration, required migration, or unsupported/manual history explicitly.
6
+ * Previous descriptors enforce immutable migration checksums and related descriptors validate
7
+ * cross-owner prerequisites. Package-owned source-to-target tests must also cover skipped versions,
8
+ * idempotency, interruption/recovery, and unsupported historical states.
9
+ * @readme
10
+ */
11
+ export declare function validateApmReleaseCandidate(candidate: ApmPackedReleaseCandidate, options?: ApmReleaseValidationOptions): ApmReleaseValidationResult;
@@ -0,0 +1,76 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { validatePackageUpdateMetadata, validateUpdateDescriptor, validateUpdateExtensionBinding, validateUpdateExtensionCapabilities, } from '@ankhorage/apm';
3
+ import { readApmReleaseMetadata } from '../utils/readApmReleaseMetadata.js';
4
+ /*** Validate packed owner evidence using the released APM protocol, never a copied schema.
5
+ * Opt-in packages declare ankh.apm with protocolVersion and a package-relative descriptor.
6
+ * No metadata means not applicable, not a claim that no migration is needed. The descriptor
7
+ * declares supported no-migration, required migration, or unsupported/manual history explicitly.
8
+ * Previous descriptors enforce immutable migration checksums and related descriptors validate
9
+ * cross-owner prerequisites. Package-owned source-to-target tests must also cover skipped versions,
10
+ * idempotency, interruption/recovery, and unsupported historical states.
11
+ * @readme
12
+ */
13
+ export function validateApmReleaseCandidate(candidate, options = {}) {
14
+ const { name, version } = candidate.packageJson;
15
+ const identity = {
16
+ ...(typeof name === 'string' ? { packageName: name } : {}),
17
+ ...(typeof version === 'string' ? { packageVersion: version } : {}),
18
+ integrity: candidate.integrity,
19
+ };
20
+ const metadata = readApmReleaseMetadata(candidate.packageJson);
21
+ const integrityBlockers = options.expectedIntegrity !== undefined && options.expectedIntegrity !== candidate.integrity
22
+ ? ['release.integrity-mismatch: Packed bytes differ from the selected release artifact.']
23
+ : [];
24
+ if (metadata === undefined) {
25
+ return {
26
+ ...identity,
27
+ applicable: false,
28
+ valid: integrityBlockers.length === 0,
29
+ blockers: integrityBlockers,
30
+ };
31
+ }
32
+ const metadataValidation = validatePackageUpdateMetadata(metadata);
33
+ if (typeof name !== 'string' || !name || typeof version !== 'string' || !version) {
34
+ return result(identity, ['release.identity: Package must define name and exact version.']);
35
+ }
36
+ if (!metadataValidation.valid)
37
+ return result(identity, [...integrityBlockers, ...messages(metadataValidation.blockers)]);
38
+ const validation = validateUpdateDescriptor({
39
+ descriptor: candidate.descriptor,
40
+ expectedOwner: { name, version },
41
+ previousDescriptors: options.previousDescriptors,
42
+ relatedDescriptors: options.relatedDescriptors,
43
+ });
44
+ if (!validation.valid || validation.descriptor === undefined || integrityBlockers.length > 0) {
45
+ return result(identity, [...integrityBlockers, ...messages(validation.blockers)]);
46
+ }
47
+ return result(identity, extensionBlockers(candidate, validation.descriptor));
48
+ }
49
+ /*** Validate executable binding and required handlers after static metadata has passed. */
50
+ function extensionBlockers(candidate, descriptor) {
51
+ if (descriptor.extension === undefined)
52
+ return [];
53
+ const artifact = {
54
+ role: 'target',
55
+ packageName: descriptor.owner.name,
56
+ version: descriptor.owner.version,
57
+ integrity: candidate.integrity,
58
+ descriptorDigest: createHash('sha256').update(candidate.descriptorSource).digest('hex'),
59
+ };
60
+ if (!isBoundExtension(artifact, candidate.extension)) {
61
+ return messages(validateUpdateExtensionBinding(artifact, candidate.extension));
62
+ }
63
+ return messages(validateUpdateExtensionCapabilities(descriptor, artifact, candidate.extension));
64
+ }
65
+ /*** Narrow unknown executable evidence only after the canonical runtime validator accepts it. */
66
+ function isBoundExtension(artifact, value) {
67
+ return validateUpdateExtensionBinding(artifact, value).length === 0;
68
+ }
69
+ /*** Preserve structured candidate identity and canonical blocker reasons. */
70
+ function result(identity, blockers) {
71
+ return { ...identity, applicable: true, valid: blockers.length === 0, blockers };
72
+ }
73
+ /*** Render the protocol's own diagnostics without reinterpreting their semantics. */
74
+ function messages(blockers) {
75
+ return blockers.map(({ code, reason }) => `${code}: ${reason}`);
76
+ }
@@ -0,0 +1,2 @@
1
+ /*** Compose descriptor version synchronization with the bounded Node filesystem adapter. */
2
+ export declare function synchronizeApmReleaseDescriptorAsync(targetDirectory: string): Promise<boolean>;
@@ -0,0 +1,6 @@
1
+ import { createApmReleaseDescriptorPort } from '../adapters/outbound/createApmReleaseDescriptorPort.js';
2
+ import { synchronizeApmReleaseMetadataAsync } from '../application/synchronizeApmReleaseMetadataAsync.js';
3
+ /*** Compose descriptor version synchronization with the bounded Node filesystem adapter. */
4
+ export async function synchronizeApmReleaseDescriptorAsync(targetDirectory) {
5
+ return synchronizeApmReleaseMetadataAsync(createApmReleaseDescriptorPort(targetDirectory));
6
+ }
@@ -0,0 +1,12 @@
1
+ import type { ApmReleaseValidationOptions, ApmReleaseValidationResult } from '../../../types/apm-release-validation.js';
2
+ /*** Validate script-free publish bytes and optionally retain that exact accepted archive.
3
+ * Release order is APM protocol, package-owner metadata/code, then consuming applications.
4
+ * Build after Changesets versioning and descriptor synchronization. Execute package-owned
5
+ * acceptance tests, then validate with explicit owner-code consent. The child probe uses the
6
+ * packed package's native public export resolution and installed dependencies, not omitted source.
7
+ * Consent permits trusted executable code; a child process is not a security sandbox.
8
+ * An artifactPath retains accepted bytes exclusively, so publication need not repack or rerun
9
+ * lifecycle scripts. expectedIntegrity rejects a stale candidate before any owner code loads.
10
+ * @readme
11
+ */
12
+ export declare function validatePackedApmReleaseAsync(targetDirectory: string, options?: ApmReleaseValidationOptions): Promise<ApmReleaseValidationResult>;
@@ -0,0 +1,14 @@
1
+ import { inspectPackedApmReleaseCandidateAsync } from '../adapters/outbound/inspectPackedApmReleaseCandidateAsync.js';
2
+ /*** Validate script-free publish bytes and optionally retain that exact accepted archive.
3
+ * Release order is APM protocol, package-owner metadata/code, then consuming applications.
4
+ * Build after Changesets versioning and descriptor synchronization. Execute package-owned
5
+ * acceptance tests, then validate with explicit owner-code consent. The child probe uses the
6
+ * packed package's native public export resolution and installed dependencies, not omitted source.
7
+ * Consent permits trusted executable code; a child process is not a security sandbox.
8
+ * An artifactPath retains accepted bytes exclusively, so publication need not repack or rerun
9
+ * lifecycle scripts. expectedIntegrity rejects a stale candidate before any owner code loads.
10
+ * @readme
11
+ */
12
+ export async function validatePackedApmReleaseAsync(targetDirectory, options = {}) {
13
+ return inspectPackedApmReleaseCandidateAsync(targetDirectory, options);
14
+ }
@@ -0,0 +1,2 @@
1
+ /*** Read opt-in owner metadata without copying the APM discovery schema. */
2
+ export declare function readApmReleaseMetadata(manifest: Readonly<Record<string, unknown>>): unknown;
@@ -0,0 +1,6 @@
1
+ import { isRecord, readOwnProperty } from '@ankhorage/utility/object';
2
+ /*** Read opt-in owner metadata without copying the APM discovery schema. */
3
+ export function readApmReleaseMetadata(manifest) {
4
+ const ankh = readOwnProperty(manifest, 'ankh');
5
+ return isRecord(ankh) ? readOwnProperty(ankh, 'apm') : undefined;
6
+ }
@@ -26,7 +26,7 @@ const REQUIRED_README_SNIPPETS = [
26
26
  'devtools.vscode.sync',
27
27
  '--dry-run',
28
28
  "profile: 'auto'",
29
- '@ankhorage/utility/project',
29
+ '@ankhorage/project-detector',
30
30
  'ankhorage-changeset',
31
31
  'without creating a second Version Packages pull request',
32
32
  'chore(release)',
@@ -3,6 +3,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import { dirname, resolve } from 'node:path';
4
4
  import { applyBunRuntimePolicy } from '../policy/applyBunRuntimePolicy.js';
5
5
  import { nodeRuntimePolicy } from '../policy/bunRuntimePolicy.js';
6
+ import { renderBunPolicyDocumentation } from '../policy/renderBunPolicyDocumentation.js';
6
7
  import { renderRenovateWorkflowAsync } from '../tools/workflows/renderRenovateWorkflowAsync.js';
7
8
  import { renderWorkflowAsync } from '../tools/workflows/renderWorkflowAsync.js';
8
9
  export async function synchronizeRenovateOwnerAsync(operation, targetDirectory, options = {}) {
@@ -57,7 +58,7 @@ async function createManagedDefinitionsAsync(targetDirectory, policy) {
57
58
  },
58
59
  {
59
60
  relativePath: 'README.md',
60
- contents: replaceReadmePolicy(readme, policy),
61
+ contents: renderBunPolicyDocumentation(readme, policy),
61
62
  },
62
63
  ];
63
64
  }
@@ -96,26 +97,6 @@ async function readTargetBunPolicyAsync(targetDirectory) {
96
97
  version,
97
98
  };
98
99
  }
99
- function replaceReadmePolicy(readme, policy) {
100
- const startIndex = readme.indexOf(README_POLICY_START);
101
- const endIndex = readme.indexOf(README_POLICY_END);
102
- if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
103
- throw new Error('README.md must contain one ordered Devtools Bun policy marker pair.');
104
- }
105
- if (readme.includes(README_POLICY_START, startIndex + README_POLICY_START.length) ||
106
- readme.includes(README_POLICY_END, endIndex + README_POLICY_END.length)) {
107
- throw new Error('README.md must contain exactly one Devtools Bun policy marker pair.');
108
- }
109
- const replacement = `${README_POLICY_START}\n\n${renderReadmePolicy(policy)}\n\n${README_POLICY_END}`;
110
- return `${readme.slice(0, startIndex)}${replacement}${readme.slice(endIndex + README_POLICY_END.length)}`;
111
- }
112
- function renderReadmePolicy(policy) {
113
- return `\`\`\`text
114
- Bun runtime ${policy.version}
115
- packageManager ${policy.packageManager}
116
- @types/bun ${policy.typesRange}
117
- \`\`\``;
118
- }
119
100
  async function runBunLockfileAsync(operation, targetDirectory) {
120
101
  const args = [
121
102
  'install',
@@ -156,5 +137,3 @@ async function syncDefinitionsAsync(targetDirectory, definitions) {
156
137
  }
157
138
  const BUN_VERSION_PATTERN = /const BUN_VERSION = '(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)';/gu;
158
139
  const BUN_TYPES_VERSION_PATTERN = /const BUN_TYPES_VERSION = '(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)';/gu;
159
- const README_POLICY_END = '<!-- devtools-bun-policy:end -->';
160
- const README_POLICY_START = '<!-- devtools-bun-policy:start -->';
@@ -0,0 +1,3 @@
1
+ import type { BunPolicy } from '../types/bunPolicy.js';
2
+ /*** Render the managed Bun policy section while preserving the surrounding guide. */
3
+ export declare function renderBunPolicyDocumentation(readme: string, policy: BunPolicy): string;
@@ -0,0 +1,24 @@
1
+ /*** Render the managed Bun policy section while preserving the surrounding guide. */
2
+ export function renderBunPolicyDocumentation(readme, policy) {
3
+ const startIndex = readme.indexOf(README_POLICY_START);
4
+ const endIndex = readme.indexOf(README_POLICY_END);
5
+ if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
6
+ throw new Error('README.md must contain one ordered Devtools Bun policy marker pair.');
7
+ }
8
+ if (readme.includes(README_POLICY_START, startIndex + README_POLICY_START.length) ||
9
+ readme.includes(README_POLICY_END, endIndex + README_POLICY_END.length)) {
10
+ throw new Error('README.md must contain exactly one Devtools Bun policy marker pair.');
11
+ }
12
+ const replacement = `${README_POLICY_START}\n\n${renderReadmePolicy(policy)}\n\n${README_POLICY_END}`;
13
+ return `${readme.slice(0, startIndex)}${replacement}${readme.slice(endIndex + README_POLICY_END.length)}`;
14
+ }
15
+ /*** Format canonical runtime and type-package versions for the guide. */
16
+ function renderReadmePolicy(policy) {
17
+ return `\`\`\`text
18
+ Bun runtime ${policy.version}
19
+ packageManager ${policy.packageManager}
20
+ @types/bun ${policy.typesRange}
21
+ \`\`\``;
22
+ }
23
+ const README_POLICY_END = '<!-- devtools-bun-policy:end -->';
24
+ const README_POLICY_START = '<!-- devtools-bun-policy:start -->';
@@ -2,7 +2,7 @@
2
2
  * Create the shared strict ESLint flat configuration used by consuming repositories.
3
3
  *
4
4
  * The default `profile: 'auto'` reads the nearest `package.json` from `tsconfigRootDir` and uses
5
- * `@ankhorage/utility/project` to select overlapping project traits. React Native and Expo select
5
+ * `@ankhorage/project-detector` to select overlapping project traits. React Native and Expo select
6
6
  * the `react-native` profile, React and Next.js select `react`, and other projects use `base`.
7
7
  *
8
8
  * Every profile includes the shared TypeScript, import, unused-import, Prettier, security, and
@@ -30,7 +30,7 @@ import securityPlugin from 'eslint-plugin-security';
30
30
  import simpleImportSort from 'eslint-plugin-simple-import-sort';
31
31
  import unusedImports from 'eslint-plugin-unused-imports';
32
32
  import tseslint from 'typescript-eslint';
33
- import { resolveEslintProfile } from './profile.js';
33
+ import { resolveEslintProfile } from './resolveEslintProfile.js';
34
34
  export const defaultIgnores = [
35
35
  '**/ios/**',
36
36
  '**/android/**',
@@ -0,0 +1,3 @@
1
+ import type { DevtoolsConfigOptions, ResolvedDevtoolsEslintProfile } from './types.js';
2
+ /*** Read the consuming package metadata and resolve its requested ESLint profile. */
3
+ export declare function resolveEslintProfile(options: DevtoolsConfigOptions): ResolvedDevtoolsEslintProfile;
@@ -1,6 +1,8 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { detectProject, } from '@ankhorage/utility/project';
2
+ import { isRecord } from '@ankhorage/utility/object';
3
3
  import { resolveProjectPackageJsonPath } from './packageJsonPath.js';
4
+ import { resolveEslintProfileFromDetectionInput } from './resolveEslintProfileFromDetectionInput.js';
5
+ /*** Read the consuming package metadata and resolve its requested ESLint profile. */
4
6
  export function resolveEslintProfile(options) {
5
7
  const requestedProfile = options.profile ?? 'auto';
6
8
  if (requestedProfile !== 'auto')
@@ -9,14 +11,7 @@ export function resolveEslintProfile(options) {
9
11
  const input = packageJsonPath === null ? {} : readDetectionInput(packageJsonPath);
10
12
  return resolveEslintProfileFromDetectionInput('auto', input);
11
13
  }
12
- export function resolveEslintProfileFromDetectionInput(requestedProfile, input) {
13
- if (requestedProfile !== 'auto')
14
- return requestedProfile;
15
- const { traits } = detectProject(input);
16
- if (traits.has('react-native') || traits.has('expo'))
17
- return 'react-native';
18
- return traits.has('react') ? 'react' : 'base';
19
- }
14
+ /*** Normalize relevant manifest fields without evaluating project configuration. */
20
15
  function readDetectionInput(packageJsonPath) {
21
16
  const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
22
17
  if (!isRecord(parsed)) {
@@ -30,20 +25,18 @@ function readDetectionInput(packageJsonPath) {
30
25
  ...(typeof parsed.packageManager === 'string' ? { packageManager: parsed.packageManager } : {}),
31
26
  };
32
27
  }
28
+ /*** Include one dependency section only when it contains a record. */
33
29
  function optionalDependencyMap(key, value) {
34
30
  const dependencyMap = toDependencyMap(value);
35
31
  return dependencyMap === undefined ? {} : { [key]: dependencyMap };
36
32
  }
33
+ /*** Retain string package ranges from one manifest dependency section. */
37
34
  function toDependencyMap(value) {
38
35
  if (!isRecord(value))
39
36
  return undefined;
40
- const dependencies = {};
41
- for (const [name, version] of Object.entries(value)) {
42
- if (typeof version === 'string')
43
- dependencies[name] = version;
44
- }
45
- return dependencies;
37
+ return Object.fromEntries(Object.entries(value).flatMap(([name, version]) => typeof version === 'string' ? [[name, version]] : []));
46
38
  }
39
+ /*** Retain runtime engine signals used by the canonical detector. */
47
40
  function optionalEngines(value) {
48
41
  if (!isRecord(value))
49
42
  return {};
@@ -53,6 +46,3 @@ function optionalEngines(value) {
53
46
  };
54
47
  return Object.keys(engines).length === 0 ? {} : { engines };
55
48
  }
56
- function isRecord(value) {
57
- return typeof value === 'object' && value !== null && !Array.isArray(value);
58
- }
@@ -0,0 +1,4 @@
1
+ import type { ProjectDetectionInput } from '@ankhorage/project-detector/types';
2
+ import type { DevtoolsEslintProfile, ResolvedDevtoolsEslintProfile } from './types.js';
3
+ /*** Apply Devtools profile precedence to traits from the canonical project detector. */
4
+ export declare function resolveEslintProfileFromDetectionInput(requestedProfile: DevtoolsEslintProfile, input: ProjectDetectionInput): ResolvedDevtoolsEslintProfile;
@@ -0,0 +1,10 @@
1
+ import { detectProject } from '@ankhorage/project-detector';
2
+ /*** Apply Devtools profile precedence to traits from the canonical project detector. */
3
+ export function resolveEslintProfileFromDetectionInput(requestedProfile, input) {
4
+ if (requestedProfile !== 'auto')
5
+ return requestedProfile;
6
+ const { traits } = detectProject(input);
7
+ if (traits.has('react-native') || traits.has('expo'))
8
+ return 'react-native';
9
+ return traits.has('react') ? 'react' : 'base';
10
+ }
@@ -41,6 +41,13 @@ jobs:
41
41
  echo "No build script found; skipping."
42
42
  fi
43
43
 
44
+ - name: Validate opted-in APM owner release
45
+ run: |
46
+ if node -e "const p=require('./package.json'); process.exit(p.ankh?.apm === undefined ? 1 : 0)"; then
47
+ bun run test:apm
48
+ __ANKH_APM_RELEASE_COMMAND__ validate . --allow-owner-code
49
+ fi
50
+
44
51
  - name: Validate Ankhorage repository
45
52
  run: bunx @ankhorage/ankh doctor validate .
46
53
 
@@ -73,6 +73,9 @@ jobs:
73
73
 
74
74
  before_version="$(node -p "require('./package.json').version")"
75
75
  __ANKH_CHANGESETS_VERSION_COMMAND__
76
+ if node -e "const p=require('./package.json'); process.exit(p.ankh?.apm === undefined ? 1 : 0)"; then
77
+ __ANKH_APM_RELEASE_COMMAND__ sync .
78
+ fi
76
79
  after_version="$(node -p "require('./package.json').version")"
77
80
 
78
81
  if git diff --quiet; then
@@ -100,7 +103,30 @@ jobs:
100
103
 
101
104
  - name: Publish unpublished packages
102
105
  if: hashFiles('.changeset/config.json') != ''
103
- run: __ANKH_CHANGESETS_PUBLISH_COMMAND__
106
+ run: |
107
+ if ! node -e "const p=require('./package.json'); process.exit(p.ankh?.apm === undefined ? 1 : 0)"; then
108
+ __ANKH_CHANGESETS_PUBLISH_COMMAND__
109
+ exit 0
110
+ fi
111
+
112
+ package_name="$(node -p "require('./package.json').name")"
113
+ package_version="$(node -p "require('./package.json').version")"
114
+ registry_evidence="${RUNNER_TEMP}/apm-registry.json"
115
+ if npm view "${package_name}@${package_version}" version --json > "$registry_evidence"; then
116
+ echo "Exact APM owner version is already published."
117
+ else
118
+ # Authentication/network errors must never be treated as an unpublished version.
119
+ node -e "const p=require(process.argv[1]); if(p.error?.code !== 'E404') process.exit(1)" "$registry_evidence"
120
+ bun install --frozen-lockfile --ignore-scripts
121
+ bun run build
122
+ bun run test:apm
123
+ artifact="$(mktemp -d "${RUNNER_TEMP}/apm-publish.XXXXXX")/candidate.tgz"
124
+ __ANKH_APM_RELEASE_COMMAND__ validate . --allow-owner-code --artifact "$artifact"
125
+ npm publish "$artifact" --ignore-scripts
126
+ fi
127
+ if ! git rev-parse -q --verify "refs/tags/v${package_version}" >/dev/null; then
128
+ git tag "v${package_version}"
129
+ fi
104
130
 
105
131
  - name: Finalize Changesets v3 tags and GitHub releases
106
132
  if: hashFiles('.changeset/config.json') != ''
@@ -1,5 +1,6 @@
1
1
  import { access } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ import { resolveApmReleaseCommandAsync } from '../../features/apm-release-validation/adapters/outbound/resolveApmReleaseCommandAsync.js';
3
4
  import { bunRuntimePolicy, nodeRuntimePolicy } from '../../policy/bunRuntimePolicy.js';
4
5
  import { renderRenovateWorkflowAsync } from './renderRenovateWorkflowAsync.js';
5
6
  import { renderWorkflowAsync } from './renderWorkflowAsync.js';
@@ -30,7 +31,8 @@ function createWorkflowDefinition(relativePath, sourcePath) {
30
31
  const sourceUrl = new URL(sourcePath, import.meta.url);
31
32
  return {
32
33
  relativePath,
33
- render: async () => await renderWorkflowAsync(sourceUrl, {
34
+ render: async (targetDirectory) => await renderWorkflowAsync(sourceUrl, {
35
+ apmReleaseCommand: await resolveApmReleaseCommandAsync(targetDirectory),
34
36
  bunVersion: bunRuntimePolicy.version,
35
37
  nodeVersion: nodeRuntimePolicy.setupVersion,
36
38
  }),
@@ -42,6 +44,7 @@ function createRenovateWorkflowDefinition() {
42
44
  return {
43
45
  relativePath: '.github/workflows/renovate.yml',
44
46
  render: async (targetDirectory) => await renderRenovateWorkflowAsync(sourceUrl, targetDirectory, {
47
+ apmReleaseCommand: await resolveApmReleaseCommandAsync(targetDirectory),
45
48
  bunVersion: bunRuntimePolicy.version,
46
49
  nodeVersion: nodeRuntimePolicy.setupVersion,
47
50
  }),
@@ -1,6 +1,7 @@
1
1
  export interface WorkflowPolicy {
2
2
  readonly bunVersion: string;
3
3
  readonly nodeVersion: string;
4
+ readonly apmReleaseCommand?: string;
4
5
  }
5
6
  /*** Renders one workflow template with the canonical runtime and Changesets policy. */
6
7
  export declare function renderWorkflowAsync(sourceUrl: URL, policy: WorkflowPolicy): Promise<string>;
@@ -8,6 +8,7 @@ export async function renderWorkflowAsync(sourceUrl, policy) {
8
8
  .replaceAll(CHANGESETS_PUBLISH_COMMAND_TOKEN, changesetsPolicy.workflowCommands.publish)
9
9
  .replaceAll(CHANGESETS_STATUS_COMMAND_TOKEN, changesetsPolicy.workflowCommands.status)
10
10
  .replaceAll(CHANGESETS_VERSION_COMMAND_TOKEN, changesetsPolicy.workflowCommands.version)
11
+ .replaceAll('__ANKH_APM_RELEASE_COMMAND__', policy.apmReleaseCommand ?? './node_modules/.bin/ankhorage-apm-release')
11
12
  .replaceAll(NODE_VERSION_TOKEN, policy.nodeVersion);
12
13
  }
13
14
  const BUN_VERSION_TOKEN = '__ANKH_BUN_VERSION__';
@@ -0,0 +1,33 @@
1
+ import type { ApmUpdateDescriptor } from '@ankhorage/apm/types';
2
+ /*** Packed bytes and loaded owner evidence supplied to the canonical APM validators. */
3
+ export interface ApmPackedReleaseCandidate {
4
+ readonly packageJson: Readonly<Record<string, unknown>>;
5
+ readonly descriptor: unknown;
6
+ readonly descriptorSource: string;
7
+ readonly integrity: string;
8
+ readonly extension?: unknown;
9
+ }
10
+ /*** Explicit release-validation inputs; owner code is never trusted by default. */
11
+ export interface ApmReleaseValidationOptions {
12
+ readonly allowOwnerCode?: boolean;
13
+ readonly artifactPath?: string;
14
+ readonly expectedIntegrity?: string;
15
+ readonly previousDescriptors?: readonly ApmUpdateDescriptor[];
16
+ readonly relatedDescriptors?: readonly ApmUpdateDescriptor[];
17
+ }
18
+ /*** Evidence for one exact packed release; inapplicable never means migration-safe. */
19
+ export interface ApmReleaseValidationResult {
20
+ readonly applicable: boolean;
21
+ readonly valid: boolean;
22
+ readonly packageName?: string;
23
+ readonly packageVersion?: string;
24
+ readonly integrity?: string;
25
+ readonly artifactPath?: string;
26
+ readonly blockers: readonly string[];
27
+ }
28
+ /*** Filesystem capabilities required by descriptor synchronization, independent of transport. */
29
+ export interface ApmReleaseDescriptorPort {
30
+ readonly readPackageAsync: () => Promise<Readonly<Record<string, unknown>>>;
31
+ readonly readDescriptorAsync: (subpath: string) => Promise<Readonly<Record<string, unknown>>>;
32
+ readonly writeDescriptorAsync: (subpath: string, descriptor: Readonly<Record<string, unknown>>) => Promise<boolean>;
33
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export interface BunPolicy {
2
+ readonly packageManager: string;
3
+ readonly typesRange: string;
4
+ readonly version: string;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export type { ApmPackedReleaseCandidate, ApmReleaseValidationOptions, ApmReleaseValidationResult, } from './apm-release-validation.js';
@@ -0,0 +1 @@
1
+ export {};