release-skill 0.1.1

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 (125) hide show
  1. package/.agents/plugins/marketplace.json +23 -0
  2. package/.claude-plugin/marketplace.json +16 -0
  3. package/.claude-plugin/plugin.json +10 -0
  4. package/.codex-plugin/plugin.json +26 -0
  5. package/CHANGELOG.md +68 -0
  6. package/CODE_OF_CONDUCT.md +76 -0
  7. package/CONTRIBUTING.md +49 -0
  8. package/INSTALL.md +182 -0
  9. package/LICENSE +21 -0
  10. package/NOTICE +25 -0
  11. package/README.md +501 -0
  12. package/README.zh-CN.md +463 -0
  13. package/SECURITY.md +48 -0
  14. package/adapters/claude/.claude-plugin/marketplace.json +16 -0
  15. package/adapters/claude/.claude-plugin/plugin.json +10 -0
  16. package/adapters/claude/skills/release-assess/SKILL.md +52 -0
  17. package/adapters/claude/skills/release-help/SKILL.md +60 -0
  18. package/adapters/claude/skills/release-prepare/SKILL.md +71 -0
  19. package/adapters/claude/skills/release-publish/SKILL.md +55 -0
  20. package/adapters/claude/skills/release-reconcile/SKILL.md +73 -0
  21. package/adapters/claude/skills/release-verify/SKILL.md +70 -0
  22. package/adapters/codex/.codex-plugin/plugin.json +26 -0
  23. package/adapters/codex/skills/release-assess/SKILL.md +52 -0
  24. package/adapters/codex/skills/release-help/SKILL.md +60 -0
  25. package/adapters/codex/skills/release-prepare/SKILL.md +71 -0
  26. package/adapters/codex/skills/release-publish/SKILL.md +55 -0
  27. package/adapters/codex/skills/release-reconcile/SKILL.md +73 -0
  28. package/adapters/codex/skills/release-verify/SKILL.md +70 -0
  29. package/bin/release-skill.mjs +743 -0
  30. package/native/safe-write/binding.gyp +40 -0
  31. package/native/safe-write/prebuilds.json +4 -0
  32. package/native/safe-write/src/safe_write.cc +2023 -0
  33. package/package.json +75 -0
  34. package/references/.render-manifest.json +33 -0
  35. package/references/00-target-state.md +124 -0
  36. package/references/01-state-machine.md +155 -0
  37. package/references/02-project-config.md +217 -0
  38. package/references/03-readme-quality.md +136 -0
  39. package/references/04-supply-chain.md +147 -0
  40. package/references/05-evidence-and-errors.md +164 -0
  41. package/references/06-adapter-contract.md +178 -0
  42. package/schemas/.render-manifest.json +37 -0
  43. package/schemas/approval-record.schema.json +115 -0
  44. package/schemas/artifact-lock.schema.json +111 -0
  45. package/schemas/artifact-plan.schema.json +52 -0
  46. package/schemas/artifact-policy.schema.json +76 -0
  47. package/schemas/evidence-event.schema.json +89 -0
  48. package/schemas/release-plan.schema.json +369 -0
  49. package/schemas/release-project.schema.json +359 -0
  50. package/schemas/release-run.schema.json +195 -0
  51. package/skills/release-assess/SKILL.md +52 -0
  52. package/skills/release-help/SKILL.md +60 -0
  53. package/skills/release-prepare/SKILL.md +71 -0
  54. package/skills/release-publish/SKILL.md +55 -0
  55. package/skills/release-reconcile/SKILL.md +73 -0
  56. package/skills/release-verify/SKILL.md +70 -0
  57. package/skills-src/release-assess/SKILL.md +52 -0
  58. package/skills-src/release-help/SKILL.md +60 -0
  59. package/skills-src/release-prepare/SKILL.md +71 -0
  60. package/skills-src/release-publish/SKILL.md +55 -0
  61. package/skills-src/release-reconcile/SKILL.md +73 -0
  62. package/skills-src/release-verify/SKILL.md +70 -0
  63. package/src/adapters/contract.mjs +214 -0
  64. package/src/adapters/git-github.mjs +214 -0
  65. package/src/adapters/npm.mjs +947 -0
  66. package/src/adapters/plugin-marketplace.mjs +1365 -0
  67. package/src/adapters/push-snapshot.mjs +216 -0
  68. package/src/artifacts/adoption.mjs +743 -0
  69. package/src/artifacts/artifact-plan.mjs +162 -0
  70. package/src/artifacts/entry.mjs +240 -0
  71. package/src/artifacts/git-authority.mjs +637 -0
  72. package/src/artifacts/graph.mjs +189 -0
  73. package/src/artifacts/inspect.mjs +520 -0
  74. package/src/artifacts/inventory.mjs +192 -0
  75. package/src/artifacts/merge/binary.mjs +77 -0
  76. package/src/artifacts/merge/entry-merge.mjs +228 -0
  77. package/src/artifacts/merge/json.mjs +641 -0
  78. package/src/artifacts/merge/markdown.mjs +246 -0
  79. package/src/artifacts/merge/regions.mjs +156 -0
  80. package/src/artifacts/merge/text.mjs +432 -0
  81. package/src/artifacts/merge/tree.mjs +202 -0
  82. package/src/artifacts/merge/yaml.mjs +669 -0
  83. package/src/artifacts/path-key.mjs +94 -0
  84. package/src/artifacts/policy.mjs +319 -0
  85. package/src/artifacts/producer-registry.mjs +439 -0
  86. package/src/artifacts/project-lock.mjs +732 -0
  87. package/src/artifacts/resolution.mjs +658 -0
  88. package/src/artifacts/safe-fs-backend-internal.mjs +680 -0
  89. package/src/artifacts/safe-fs.mjs +72 -0
  90. package/src/artifacts/state.mjs +495 -0
  91. package/src/artifacts/transaction-journal.mjs +983 -0
  92. package/src/artifacts/transaction.mjs +1361 -0
  93. package/src/commands/approve.mjs +280 -0
  94. package/src/commands/artifacts.mjs +627 -0
  95. package/src/commands/assess.mjs +838 -0
  96. package/src/commands/prepare.mjs +1377 -0
  97. package/src/commands/publish.mjs +883 -0
  98. package/src/commands/reconcile.mjs +1255 -0
  99. package/src/commands/verify.mjs +915 -0
  100. package/src/core/approval.mjs +332 -0
  101. package/src/core/baseline.mjs +272 -0
  102. package/src/core/blackbox-hard-gates.mjs +142 -0
  103. package/src/core/config.mjs +448 -0
  104. package/src/core/digest.mjs +90 -0
  105. package/src/core/errors.mjs +113 -0
  106. package/src/core/evidence.mjs +167 -0
  107. package/src/core/hooks.mjs +241 -0
  108. package/src/core/node-version.mjs +64 -0
  109. package/src/core/plan.mjs +735 -0
  110. package/src/core/previous-public-baseline.mjs +204 -0
  111. package/src/core/run.mjs +681 -0
  112. package/src/core/state-machine.mjs +76 -0
  113. package/src/core/version-consistency.mjs +111 -0
  114. package/src/producers/build-adapters.mjs +231 -0
  115. package/src/producers/render-public-assets.mjs +152 -0
  116. package/src/producers/sync-skills.mjs +96 -0
  117. package/src/readme/contract.mjs +297 -0
  118. package/src/readme/examples.mjs +288 -0
  119. package/src/readme/parity.mjs +122 -0
  120. package/src/snapshot/export.mjs +99 -0
  121. package/src/snapshot/frozen.mjs +401 -0
  122. package/src/snapshot/manifest.mjs +207 -0
  123. package/src/snapshot/public-map.mjs +1459 -0
  124. package/src/snapshot/public-path.mjs +110 -0
  125. package/src/snapshot/scan.mjs +419 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Adapter unified interface contract for release-skill.
3
+ *
4
+ * Every platform adapter (git-github, npm, plugin-marketplace) must implement
5
+ * the four lifecycle methods: preflight, execute, observe, verify.
6
+ *
7
+ * Adapters return structured observations and NEVER infer success from
8
+ * command exit codes alone. A checkpoint is successful only when `observe`
9
+ * matches the frozen expected commit/tag/version/digest in the release plan.
10
+ *
11
+ * All write methods MUST require `context.externalWritesAuthorized === true`.
12
+ *
13
+ * @module adapters/contract
14
+ */
15
+
16
+ import { ReleaseError, AUTH_MISSING } from '../core/errors.mjs';
17
+
18
+ /**
19
+ * Action status values in the action lifecycle.
20
+ * @enum {string}
21
+ */
22
+ export const ActionStatus = Object.freeze({
23
+ PENDING: 'PENDING',
24
+ PREFLIGHT_PASSED: 'PREFLIGHT_PASSED',
25
+ PREFLIGHT_FAILED: 'PREFLIGHT_FAILED',
26
+ EXECUTING: 'EXECUTING',
27
+ EXECUTED: 'EXECUTED',
28
+ EXECUTE_FAILED: 'EXECUTE_FAILED',
29
+ OBSERVED: 'OBSERVED',
30
+ OBSERVE_MISMATCH: 'OBSERVE_MISMATCH',
31
+ VERIFIED: 'VERIFIED',
32
+ VERIFY_FAILED: 'VERIFY_FAILED',
33
+ SKIPPED: 'SKIPPED',
34
+ });
35
+
36
+ /**
37
+ * Standard action types across all adapters.
38
+ * @enum {string}
39
+ */
40
+ export const ActionType = Object.freeze({
41
+ // git-github adapter
42
+ GIT_PUSH: 'git-push',
43
+ GIT_TAG: 'git-tag',
44
+ GITHUB_RELEASE: 'github-release',
45
+
46
+ // npm adapter
47
+ NPM_PACK: 'npm-pack',
48
+ NPM_PUBLISH: 'npm-publish',
49
+
50
+ // snapshot push
51
+ PUSH_SNAPSHOT: 'push-snapshot',
52
+
53
+ // plugin-marketplace adapter
54
+ PLUGIN_MANIFEST_VALIDATE: 'plugin-manifest-validate',
55
+ PLUGIN_INSTALL_CHECK: 'plugin-install-check',
56
+
57
+ // consumer marketplace install (production)
58
+ CLAUDE_MARKETPLACE_INSTALL: 'claude-marketplace-install',
59
+ CODEX_MARKETPLACE_INSTALL: 'codex-marketplace-install',
60
+ });
61
+
62
+ /**
63
+ * Create a structured adapter result object.
64
+ *
65
+ * @param {Object} params
66
+ * @param {string} params.actionType - The ActionType of the action.
67
+ * @param {string} params.status - One of ActionStatus values.
68
+ * @param {Object} [params.observation] - Observed remote state (commit, tag, version, digest, etc.).
69
+ * @param {string} [params.error] - Error message if status indicates failure.
70
+ * @param {Object} [params.details] - Additional machine-readable details.
71
+ * @returns {AdapterResult}
72
+ */
73
+ export function createResult({ actionType, status, observation, error, details }) {
74
+ return Object.freeze({
75
+ actionType,
76
+ status,
77
+ observation: observation ?? null,
78
+ error: error ?? null,
79
+ details: details ?? null,
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Check that external writes are authorized. Throws if not.
85
+ *
86
+ * @param {AdapterContext} context
87
+ * @param {string} actionType - The action being guarded.
88
+ * @throws {ReleaseError} AUTH_MISSING if not authorized.
89
+ */
90
+ export function assertWritesAuthorized(context, actionType) {
91
+ if (context.externalWritesAuthorized !== true) {
92
+ throw new ReleaseError(
93
+ AUTH_MISSING,
94
+ `External write action '${actionType}' requires externalWritesAuthorized === true`,
95
+ { actionType }
96
+ );
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Check that isolated consumer writes are authorized. Throws if not.
102
+ * Used by marketplace install adapters which only write to isolated
103
+ * consumer directories, not to remote services.
104
+ *
105
+ * @param {AdapterContext} context
106
+ * @param {string} actionType - The action being guarded.
107
+ * @throws {ReleaseError} AUTH_MISSING if not authorized.
108
+ */
109
+ export function assertIsolatedConsumerWritesAuthorized(context, actionType) {
110
+ if (context.isolatedConsumerWritesAuthorized !== true) {
111
+ throw new ReleaseError(
112
+ AUTH_MISSING,
113
+ `Marketplace install action '${actionType}' requires isolatedConsumerWritesAuthorized === true`,
114
+ { actionType }
115
+ );
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Validate that an observation matches the expected state from a frozen plan.
121
+ *
122
+ * The expected object comes from the release plan's externalActions[].expected.
123
+ * An observation matches when every key present in expected has the same value
124
+ * in observation. Extra keys in observation are allowed.
125
+ *
126
+ * @param {Object} expected - Expected state from the frozen plan.
127
+ * @param {Object} observation - Observed state from the remote.
128
+ * @returns {{ matches: boolean, mismatches: string[] }}
129
+ */
130
+ export function matchObservation(expected, observation) {
131
+ if (!expected || !observation) {
132
+ return { matches: false, mismatches: ['missing expected or observation'] };
133
+ }
134
+
135
+ const mismatches = [];
136
+ for (const [key, expectedValue] of Object.entries(expected)) {
137
+ if (observation[key] !== expectedValue) {
138
+ mismatches.push(`${key}: expected ${JSON.stringify(expectedValue)}, got ${JSON.stringify(observation[key])}`);
139
+ }
140
+ }
141
+
142
+ return { matches: mismatches.length === 0, mismatches };
143
+ }
144
+
145
+ /**
146
+ * Adapter interface type documentation (not enforced at runtime, but all
147
+ * adapters must follow this shape):
148
+ *
149
+ * @typedef {Object} AdapterResult
150
+ * @property {string} actionType - The action type.
151
+ * @property {string} status - ActionStatus value.
152
+ * @property {Object|null} observation - Observed remote state.
153
+ * @property {string|null} error - Error message if failed.
154
+ * @property {Object|null} details - Additional context.
155
+ *
156
+ * @typedef {Object} AdapterContext
157
+ * @property {boolean} externalWritesAuthorized - Must be true for remote write actions.
158
+ * @property {boolean} [isolatedConsumerWritesAuthorized] - Must be true for marketplace install actions.
159
+ * @property {Object} plan - The frozen release plan.
160
+ * @property {Object} baseline - The captured git baseline.
161
+ * @property {string} root - The project root directory.
162
+ * @property {string} [runDir] - The evidence run directory.
163
+ * @property {Object} [env] - Environment variables.
164
+ *
165
+ * @typedef {Object} Adapter
166
+ * @property {string} name - Adapter display name.
167
+ * @property {string[]} actionTypes - Supported ActionType values.
168
+ * @property {(action: Object, context: AdapterContext) => Promise<AdapterResult>} preflight
169
+ * @property {(action: Object, context: AdapterContext) => Promise<AdapterResult>} execute
170
+ * @property {(action: Object, context: AdapterContext) => Promise<AdapterResult>} observe
171
+ * @property {(action: Object, context: AdapterContext) => Promise<AdapterResult>} verify
172
+ */
173
+
174
+ /**
175
+ * Create an adapter registry that maps action types to adapter instances.
176
+ *
177
+ * @param {Adapter[]} adapters - Array of adapter instances.
178
+ * @returns {{ getAdapter(actionType: string): Adapter, getAll(): Adapter[] }}
179
+ */
180
+ export function createAdapterRegistry(adapters) {
181
+ const actionMap = new Map();
182
+
183
+ for (const adapter of adapters) {
184
+ for (const actionType of adapter.actionTypes) {
185
+ if (actionMap.has(actionType)) {
186
+ throw new Error(
187
+ `Duplicate action type '${actionType}' registered by both '${actionMap.get(actionType).name}' and '${adapter.name}'`
188
+ );
189
+ }
190
+ actionMap.set(actionType, adapter);
191
+ }
192
+ }
193
+
194
+ return Object.freeze({
195
+ /**
196
+ * Get the adapter responsible for a given action type.
197
+ * @param {string} actionType
198
+ * @returns {Adapter}
199
+ * @throws {Error} if no adapter handles this action type.
200
+ */
201
+ getAdapter(actionType) {
202
+ const adapter = actionMap.get(actionType);
203
+ if (!adapter) {
204
+ throw new Error(`No adapter registered for action type '${actionType}'`);
205
+ }
206
+ return adapter;
207
+ },
208
+
209
+ /** Get all registered adapters. */
210
+ getAll() {
211
+ return [...new Set(actionMap.values())];
212
+ },
213
+ });
214
+ }
@@ -0,0 +1,214 @@
1
+ import { execFile as execFileCb } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ import {
5
+ ActionStatus,
6
+ ActionType,
7
+ assertWritesAuthorized,
8
+ createResult,
9
+ matchObservation,
10
+ } from './contract.mjs';
11
+ import { resolveFrozenPath, verifyFrozenDirectoryStructure } from '../snapshot/frozen.mjs';
12
+ import { githubRepositoryUrl } from './push-snapshot.mjs';
13
+
14
+ const execFile = promisify(execFileCb);
15
+ const NAME = 'git-github';
16
+
17
+ async function run(command, args, options = {}) {
18
+ return execFile(command, args, {
19
+ shell: false,
20
+ encoding: 'utf8',
21
+ timeout: 120_000,
22
+ ...options,
23
+ });
24
+ }
25
+
26
+ function isNotFound(error) {
27
+ const text = `${error?.stdout ?? ''}\n${error?.stderr ?? ''}\n${error?.message ?? ''}`;
28
+ return /(?:HTTP\s+404|\bstatus(?: code)?\s*[:=]?\s*404\b)/i.test(text);
29
+ }
30
+
31
+ function ghRepo(action) {
32
+ const host = action.githubHost ?? 'github.com';
33
+ githubRepositoryUrl(action.repo, host);
34
+ return host === 'github.com' ? action.repo : `${host}/${action.repo}`;
35
+ }
36
+
37
+ function validateOid(value) {
38
+ if (typeof value !== 'string' || !/^[a-f0-9]{40,64}$/.test(value)) {
39
+ throw new Error('commit must be a full hexadecimal git object id');
40
+ }
41
+ }
42
+
43
+ async function validateTagAction(action, context, exec) {
44
+ if (!action.tag || !action.repo || !action.gitObjectDir) {
45
+ throw new Error('git-tag requires tag, repo, gitObjectDir, and commit');
46
+ }
47
+ validateOid(action.commit);
48
+ const gitDir = await resolveFrozenPath(context.root, action.gitObjectDir, 'frozen git object directory');
49
+ await verifyFrozenDirectoryStructure(gitDir, 'frozen git object directory');
50
+ await exec('git', ['check-ref-format', `refs/tags/${action.tag}`], { shell: false });
51
+ const { stdout: commitOut } = await exec('git', ['--git-dir', gitDir, 'rev-parse', action.commit], { shell: false });
52
+ if (commitOut.trim() !== action.commit) throw new Error('planned commit is missing from frozen git objects');
53
+ return { gitDir, remoteUrl: githubRepositoryUrl(action.repo, action.githubHost) };
54
+ }
55
+
56
+ async function readRemoteTag(action, remoteUrl, exec) {
57
+ const { stdout } = await exec(
58
+ 'git',
59
+ ['ls-remote', '--tags', remoteUrl, `refs/tags/${action.tag}`],
60
+ { shell: false },
61
+ );
62
+ return stdout.trim().split(/\s+/)[0] ?? '';
63
+ }
64
+
65
+ export function createGitGithubAdapter(deps = {}) {
66
+ const exec = deps.exec ?? run;
67
+ return Object.freeze({
68
+ name: NAME,
69
+ actionTypes: Object.freeze([ActionType.GIT_PUSH, ActionType.GIT_TAG, ActionType.GITHUB_RELEASE]),
70
+
71
+ async preflight(action, context) {
72
+ try {
73
+ if (action.actionType === ActionType.GIT_PUSH) {
74
+ const { stdout } = await exec('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: context.root, shell: false });
75
+ const expected = action.branch ?? context.plan?.baseline?.defaultBranch ?? 'main';
76
+ if (stdout.trim() !== expected) throw new Error(`current branch does not match ${expected}`);
77
+ } else if (action.actionType === ActionType.GIT_TAG) {
78
+ const { remoteUrl } = await validateTagAction(action, context, exec);
79
+ const existing = await readRemoteTag(action, remoteUrl, exec);
80
+ if (existing) throw new Error(`remote tag already exists: ${action.tag}; human intervention required`);
81
+ } else if (action.actionType === ActionType.GITHUB_RELEASE) {
82
+ if (!action.repo || !action.tag) throw new Error('github-release requires repo, tag, and commit');
83
+ validateOid(action.commit);
84
+ await exec('gh', ['auth', 'status'], {
85
+ cwd: context.root,
86
+ shell: false,
87
+ env: { ...process.env, GH_HOST: action.githubHost ?? 'github.com' },
88
+ });
89
+ await exec('gh', ['api', `repos/${action.repo}`, '--jq', '.id'], {
90
+ cwd: context.root,
91
+ shell: false,
92
+ env: { ...process.env, GH_HOST: action.githubHost ?? 'github.com' },
93
+ }).catch((err) => {
94
+ throw new Error(`cannot access GitHub repository ${action.repo}: ${err.message}`);
95
+ });
96
+ try {
97
+ await exec('gh', ['api', `repos/${action.repo}/releases/tags/${action.tag}`], {
98
+ cwd: context.root,
99
+ shell: false,
100
+ env: { ...process.env, GH_HOST: action.githubHost ?? 'github.com' },
101
+ });
102
+ throw new Error(`GitHub Release already exists for ${action.tag}; human intervention required`);
103
+ } catch (err) {
104
+ if (err.message?.includes('already exists')) throw err;
105
+ if (!isNotFound(err)) throw new Error(`cannot determine GitHub Release uniqueness: ${err.message}`);
106
+ }
107
+ } else {
108
+ throw new Error(`unsupported action type: ${action.actionType}`);
109
+ }
110
+ return createResult({ actionType: action.actionType, status: ActionStatus.PREFLIGHT_PASSED });
111
+ } catch (err) {
112
+ return createResult({ actionType: action.actionType, status: ActionStatus.PREFLIGHT_FAILED, error: err.message });
113
+ }
114
+ },
115
+
116
+ async execute(action, context) {
117
+ assertWritesAuthorized(context, action.actionType);
118
+ try {
119
+ if (action.actionType === ActionType.GIT_PUSH) {
120
+ await exec('git', ['push', action.remote ?? 'origin', action.branch ?? 'main'], {
121
+ cwd: context.root,
122
+ shell: false,
123
+ });
124
+ } else if (action.actionType === ActionType.GIT_TAG) {
125
+ const { gitDir, remoteUrl } = await validateTagAction(action, context, exec);
126
+ let localTag = '';
127
+ try {
128
+ const result = await exec('git', ['--git-dir', gitDir, 'rev-parse', `refs/tags/${action.tag}`], { shell: false });
129
+ localTag = result.stdout.trim();
130
+ } catch {
131
+ localTag = '';
132
+ }
133
+ if (localTag && localTag !== action.commit) throw new Error('local frozen tag conflicts with planned commit');
134
+ if (!localTag) {
135
+ await exec('git', ['--git-dir', gitDir, 'tag', action.tag, action.commit], { shell: false });
136
+ }
137
+ await exec('git', [
138
+ '--git-dir', gitDir,
139
+ 'push',
140
+ `--force-with-lease=refs/tags/${action.tag}:`,
141
+ remoteUrl,
142
+ `refs/tags/${action.tag}`,
143
+ ], { shell: false });
144
+ } else if (action.actionType === ActionType.GITHUB_RELEASE) {
145
+ await exec('gh', [
146
+ 'release', 'create', action.tag,
147
+ '--repo', ghRepo(action),
148
+ '--verify-tag',
149
+ '--title', action.name ?? `Release ${action.tag}`,
150
+ '--notes', action.notes ?? `Release ${action.tag}`,
151
+ ], { cwd: context.root, shell: false, env: { ...process.env, GH_HOST: action.githubHost ?? 'github.com' } });
152
+ } else {
153
+ throw new Error(`unsupported action type: ${action.actionType}`);
154
+ }
155
+ return createResult({ actionType: action.actionType, status: ActionStatus.EXECUTED });
156
+ } catch (err) {
157
+ return createResult({ actionType: action.actionType, status: ActionStatus.EXECUTE_FAILED, error: err.message });
158
+ }
159
+ },
160
+
161
+ async observe(action, context) {
162
+ try {
163
+ let observation;
164
+ if (action.actionType === ActionType.GIT_PUSH) {
165
+ const { stdout } = await exec('git', ['ls-remote', '--heads', action.remote ?? 'origin', action.branch ?? 'main'], {
166
+ cwd: context.root,
167
+ shell: false,
168
+ });
169
+ observation = { remoteCommit: stdout.trim().split(/\s+/)[0] ?? '' };
170
+ } else if (action.actionType === ActionType.GIT_TAG) {
171
+ const { remoteUrl } = await validateTagAction(action, context, exec);
172
+ observation = { tag: action.tag, commit: await readRemoteTag(action, remoteUrl, exec) };
173
+ } else if (action.actionType === ActionType.GITHUB_RELEASE) {
174
+ const { stdout } = await exec('gh', [
175
+ 'release', 'view', action.tag,
176
+ '--repo', ghRepo(action),
177
+ '--json', 'tagName,url',
178
+ ], { cwd: context.root, shell: false, env: { ...process.env, GH_HOST: action.githubHost ?? 'github.com' } });
179
+ const release = JSON.parse(stdout);
180
+ const commitResult = await exec(
181
+ 'gh',
182
+ ['api', `repos/${action.repo}/git/ref/tags/${action.tag}`, '--jq', '.object.sha'],
183
+ { cwd: context.root, shell: false, env: { ...process.env, GH_HOST: action.githubHost ?? 'github.com' } },
184
+ );
185
+ observation = { tag: release.tagName, commit: commitResult.stdout.trim(), releaseUrl: release.url };
186
+ } else {
187
+ throw new Error(`unsupported action type: ${action.actionType}`);
188
+ }
189
+ return createResult({ actionType: action.actionType, status: ActionStatus.OBSERVED, observation });
190
+ } catch (err) {
191
+ if (action.actionType === ActionType.GITHUB_RELEASE && isNotFound(err)) {
192
+ return createResult({
193
+ actionType: action.actionType,
194
+ status: ActionStatus.OBSERVED,
195
+ observation: { exists: false },
196
+ });
197
+ }
198
+ return createResult({ actionType: action.actionType, status: ActionStatus.OBSERVED, observation: {}, error: err.message });
199
+ }
200
+ },
201
+
202
+ async verify(action, context) {
203
+ const observed = await this.observe(action, context);
204
+ if (observed.error) return createResult({ actionType: action.actionType, status: ActionStatus.VERIFY_FAILED, observation: observed.observation, error: observed.error });
205
+ const comparison = matchObservation(action.expected ?? {}, observed.observation);
206
+ return createResult({
207
+ actionType: action.actionType,
208
+ status: comparison.matches ? ActionStatus.VERIFIED : ActionStatus.VERIFY_FAILED,
209
+ observation: observed.observation,
210
+ error: comparison.matches ? null : comparison.mismatches.join('; '),
211
+ });
212
+ },
213
+ });
214
+ }