@postman-cse/onboarding-repo-sync 2.1.9 → 2.1.13

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": "@postman-cse/onboarding-repo-sync",
3
- "version": "2.1.9",
3
+ "version": "2.1.13",
4
4
  "description": "Postman repo sync GitHub Action.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -9,21 +9,23 @@
9
9
  },
10
10
  "files": [
11
11
  "action.yml",
12
- "dist"
12
+ "dist",
13
+ "scripts/verify-release-artifacts.mjs"
13
14
  ],
14
15
  "engines": {
15
16
  "node": ">=24"
16
17
  },
17
18
  "scripts": {
18
19
  "prepare": "git config core.hooksPath .githooks",
19
- "bundle": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && esbuild src/index.ts --bundle --platform=node --target=node24 --format=cjs --outfile=dist/index.cjs && esbuild src/main.ts --bundle --platform=node --target=node24 --format=cjs --outfile=dist/action.cjs && esbuild src/cli.ts --bundle --platform=node --target=node24 --format=cjs --banner:js=\"#!/usr/bin/env node\" --outfile=dist/cli.cjs && node -e \"if(process.platform!=='win32')require('node:fs').chmodSync('dist/cli.cjs',0o755)\"",
20
+ "bundle": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && esbuild src/index.ts --bundle --platform=node --target=node24 --format=cjs --define:__SEA_VERSION__=undefined --outfile=dist/index.cjs && esbuild src/main.ts --bundle --platform=node --target=node24 --format=cjs --define:__SEA_VERSION__=undefined --outfile=dist/action.cjs && esbuild src/cli.ts --bundle --platform=node --target=node24 --format=cjs --define:__SEA_VERSION__=undefined --banner:js=\"#!/usr/bin/env node\" --outfile=dist/cli.cjs && node -e \"if(process.platform!=='win32')require('node:fs').chmodSync('dist/cli.cjs',0o755)\"",
20
21
  "build": "npm run typecheck && npm run bundle",
22
+ "build:sea": "bash scripts/build-sea.sh",
21
23
  "verify:dist:assert": "git diff --ignore-space-at-eol --text --exit-code -- dist && node scripts/verify-dist-artifact.mjs",
22
24
  "verify:dist": "npm run build && npm run verify:dist:assert",
23
25
  "docs:tables": "node scripts/render-action-tables.mjs",
24
26
  "lint": "eslint .",
25
27
  "lint:fix": "eslint . --fix",
26
- "test": "vitest run && node --test .github/scripts/wait-for-e2e-gate.test.mjs",
28
+ "test": "vitest run && node --test .github/scripts/dispatch-e2e-monitor.test.mjs",
27
29
  "typecheck": "tsc --noEmit -p tsconfig.json"
28
30
  },
29
31
  "keywords": [
@@ -35,29 +37,31 @@
35
37
  "access": "public"
36
38
  },
37
39
  "overrides": {
38
- "undici": "^6.24.0",
39
- "picomatch": ">=4.0.4"
40
+ "undici": "^8.8.0",
41
+ "picomatch": ">=4.0.4",
42
+ "uuid": "11.1.1"
40
43
  },
41
44
  "dependencies": {
42
45
  "@actions/core": "^3.0.1",
43
46
  "@actions/exec": "^3.0.0",
44
47
  "@postman-cse/automation-telemetry-core": "^0.2.0",
45
- "@postman/runtime.models": "^1.11.1",
46
- "@postman/v3.export": "^0.2.25",
48
+ "@postman/runtime.models": "^1.13.1",
49
+ "@postman/v3.export": "^0.2.28",
47
50
  "js-yaml": "^5.2.1",
48
- "undici": "^6.24.0"
51
+ "undici": "^8.8.0"
49
52
  },
50
53
  "devDependencies": {
51
- "@commitlint/cli": "^21.0.2",
52
- "@commitlint/config-conventional": "^21.0.2",
54
+ "@commitlint/cli": "^21.2.1",
55
+ "@commitlint/config-conventional": "^21.2.0",
53
56
  "@eslint/js": "^10.0.1",
54
57
  "@types/js-yaml": "^4.0.9",
55
58
  "@types/node": "^26.1.0",
56
59
  "esbuild": "0.28.1",
57
- "eslint": "^10.4.1",
60
+ "eslint": "^10.7.0",
61
+ "postject": "1.0.0-alpha.6",
58
62
  "typescript": "^6.0.3",
59
- "typescript-eslint": "^8.60.0",
60
- "vitest": "^4.1.7",
63
+ "typescript-eslint": "^8.65.0",
64
+ "vitest": "^4.1.10",
61
65
  "yaml": "^2.9.0"
62
66
  },
63
67
  "repository": {
@@ -0,0 +1,239 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
4
+ import { basename, join } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ const SAFE_BASENAME = /^[A-Za-z0-9._-]+$/;
8
+ const SHA256_HEX = /^[a-f0-9]{64}$/;
9
+
10
+ /**
11
+ * @param {Buffer|string} bytes
12
+ * @returns {string}
13
+ */
14
+ export function sha256Hex(bytes) {
15
+ return createHash('sha256').update(bytes).digest('hex');
16
+ }
17
+
18
+ /**
19
+ * @param {Buffer|string} bytes
20
+ * @returns {string}
21
+ */
22
+ export function computeNpmSri(bytes) {
23
+ return `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
24
+ }
25
+
26
+ /**
27
+ * @param {string} expected
28
+ * @param {string} actual
29
+ */
30
+ export function assertNpmSriMatch(expected, actual) {
31
+ if (expected !== actual) {
32
+ throw new Error('existing npm package integrity differs from staged tarball');
33
+ }
34
+ }
35
+
36
+ /**
37
+ * @param {string} tag
38
+ * @param {string} packageVersion
39
+ */
40
+ export function validateTagVersion(tag, packageVersion) {
41
+ const [major, minor, patch] = packageVersion.split('.');
42
+ if (tag !== `v${packageVersion}` && !(patch === '0' && tag === `v${major}.${minor}`)) {
43
+ throw new Error(`tag ${tag} does not match package version ${packageVersion}`);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * @param {string} tag
49
+ * @param {string} version
50
+ * @returns {boolean}
51
+ */
52
+ export function validateReleaseTag(tag, version) {
53
+ try {
54
+ validateTagVersion(tag, version);
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * @param {string} packageVersion
63
+ * @returns {string[]}
64
+ */
65
+ export function expectedArtifactNames(packageVersion) {
66
+ const sea = `postman-repo-sync-${packageVersion}-linux-x64`;
67
+ return ['release.tgz', sea, `${sea}.sha256`];
68
+ }
69
+
70
+ /**
71
+ * @param {unknown} value
72
+ * @param {string} label
73
+ */
74
+ function assertString(value, label) {
75
+ if (typeof value !== 'string' || value.length === 0) {
76
+ throw new Error(`manifest ${label} must be a non-empty string`);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * @param {unknown} manifest
82
+ * @param {string} directory
83
+ * @param {{ repository: string, commitSha: string, tag: string, packageName?: string, packageVersion?: string }} expected
84
+ */
85
+ export function validateManifest(manifest, directory, expected) {
86
+ if (!manifest || typeof manifest !== 'object') throw new Error('invalid manifest schema');
87
+ const body = /** @type {Record<string, unknown>} */ (manifest);
88
+ if (body.schema_version !== 1) throw new Error('invalid manifest schema');
89
+ assertString(body.repository, 'repository');
90
+ assertString(body.commit_sha, 'commit_sha');
91
+ assertString(body.tag, 'tag');
92
+ assertString(body.package_name, 'package_name');
93
+ assertString(body.package_version, 'package_version');
94
+ if (!Array.isArray(body.artifacts)) throw new Error('invalid manifest schema');
95
+
96
+ for (const [key, value] of Object.entries({
97
+ repository: expected.repository,
98
+ commit_sha: expected.commitSha,
99
+ tag: expected.tag
100
+ })) {
101
+ if (body[key] !== value) throw new Error(`manifest ${key} mismatch`);
102
+ }
103
+ if (expected.packageName !== undefined && body.package_name !== expected.packageName) {
104
+ throw new Error('manifest package_name mismatch');
105
+ }
106
+ if (expected.packageVersion !== undefined && body.package_version !== expected.packageVersion) {
107
+ throw new Error('manifest package_version mismatch');
108
+ }
109
+
110
+ const packageVersion = String(body.package_version);
111
+ const expectedNames = expectedArtifactNames(packageVersion);
112
+ const seen = new Set();
113
+ /** @type {Array<{ path: string, sha256: string }>} */
114
+ const artifacts = [];
115
+ for (const entry of body.artifacts) {
116
+ if (!entry || typeof entry !== 'object') throw new Error('invalid artifact manifest entry');
117
+ const artifact = /** @type {Record<string, unknown>} */ (entry);
118
+ assertString(artifact.path, 'artifact.path');
119
+ assertString(artifact.sha256, 'artifact.sha256');
120
+ const path = String(artifact.path);
121
+ const digest = String(artifact.sha256);
122
+ if (
123
+ path !== basename(path) ||
124
+ path.includes('..') ||
125
+ path.includes('/') ||
126
+ path.includes('\\') ||
127
+ !SAFE_BASENAME.test(path)
128
+ ) {
129
+ throw new Error(`unsafe artifact path ${path}`);
130
+ }
131
+ if (!SHA256_HEX.test(digest)) throw new Error(`invalid artifact sha256 for ${path}`);
132
+ if (seen.has(path)) throw new Error(`duplicate artifact path ${path}`);
133
+ seen.add(path);
134
+ artifacts.push({ path, sha256: digest });
135
+ }
136
+
137
+ if (artifacts.length !== expectedNames.length || expectedNames.some((name) => !seen.has(name))) {
138
+ throw new Error(`exact artifact allowlist mismatch; expected ${expectedNames.join(', ')}`);
139
+ }
140
+
141
+ const onDisk = new Set(readdirSync(directory));
142
+ if (!onDisk.has('release-manifest.json')) throw new Error('missing release-manifest.json');
143
+ for (const name of expectedNames) {
144
+ if (!onDisk.has(name)) throw new Error(`missing artifact ${name}`);
145
+ }
146
+ for (const name of onDisk) {
147
+ if (name === 'release-manifest.json') continue;
148
+ if (!seen.has(name)) throw new Error(`unexpected filesystem entry ${name}`);
149
+ }
150
+
151
+ for (const artifact of artifacts) {
152
+ const fullPath = join(directory, artifact.path);
153
+ if (!existsSync(fullPath)) throw new Error(`missing artifact ${artifact.path}`);
154
+ if (sha256Hex(readFileSync(fullPath)) !== artifact.sha256) {
155
+ throw new Error(`checksum mismatch for ${artifact.path}`);
156
+ }
157
+ }
158
+
159
+ validateTagVersion(String(body.tag), packageVersion);
160
+ validateSeaSidecar(directory, packageVersion, artifacts);
161
+ return body;
162
+ }
163
+
164
+ /**
165
+ * @param {string} directory
166
+ * @param {string} packageVersion
167
+ * @param {Array<{ path: string, sha256: string }>} artifacts
168
+ */
169
+ export function validateSeaSidecar(directory, packageVersion, artifacts) {
170
+ const sea = `postman-repo-sync-${packageVersion}-linux-x64`;
171
+ const sidecarName = `${sea}.sha256`;
172
+ const seaEntry = artifacts.find((artifact) => artifact.path === sea);
173
+ const sidecarEntry = artifacts.find((artifact) => artifact.path === sidecarName);
174
+ if (!seaEntry || !sidecarEntry) throw new Error('SEA executable and sidecar are required');
175
+ const sidecarText = readFileSync(join(directory, sidecarName), 'utf8').trim();
176
+ const [digest = '', filename = ''] = sidecarText.split(/\s+/);
177
+ if (!SHA256_HEX.test(digest) || filename !== sea) {
178
+ throw new Error(`SEA sidecar text must be "<sha256> ${sea}"`);
179
+ }
180
+ const actual = sha256Hex(readFileSync(join(directory, sea)));
181
+ if (
182
+ digest !== actual ||
183
+ digest !== seaEntry.sha256 ||
184
+ sidecarEntry.sha256 !== sha256Hex(readFileSync(join(directory, sidecarName)))
185
+ ) {
186
+ throw new Error('SEA sidecar digest does not match executable and manifest');
187
+ }
188
+ }
189
+
190
+ /**
191
+ * @param {string} directory
192
+ */
193
+ export function readTarballPackageIdentity(directory) {
194
+ const tarball = join(directory, 'release.tgz');
195
+ const packageJson = JSON.parse(
196
+ execFileSync('tar', ['-xOf', tarball, 'package/package.json'], {
197
+ encoding: 'utf8',
198
+ stdio: ['ignore', 'pipe', 'pipe']
199
+ })
200
+ );
201
+ return { name: packageJson.name, version: packageJson.version };
202
+ }
203
+
204
+ /**
205
+ * @param {{ directory?: string, repository: string, commitSha: string, tag: string }} input
206
+ */
207
+ export function verifyReleaseArtifacts({ directory = '.', repository, commitSha, tag }) {
208
+ const manifestPath = join(directory, 'release-manifest.json');
209
+ if (!existsSync(manifestPath)) throw new Error('release-manifest.json is required');
210
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
211
+ const identity = readTarballPackageIdentity(directory);
212
+ validateManifest(manifest, directory, {
213
+ repository,
214
+ commitSha,
215
+ tag,
216
+ packageName: identity.name,
217
+ packageVersion: identity.version
218
+ });
219
+ if (manifest.package_name !== identity.name || manifest.package_version !== identity.version) {
220
+ throw new Error('tarball package identity mismatch');
221
+ }
222
+ return manifest;
223
+ }
224
+
225
+ function main() {
226
+ const directory = process.argv[2];
227
+ if (!directory) throw new Error('usage: node scripts/verify-release-artifacts.mjs <directory>');
228
+ verifyReleaseArtifacts({
229
+ directory,
230
+ repository: process.env.GITHUB_REPOSITORY ?? '',
231
+ commitSha: process.env.GITHUB_SHA ?? '',
232
+ tag: process.env.GITHUB_REF_NAME ?? ''
233
+ });
234
+ console.log('release artifact manifest verified');
235
+ }
236
+
237
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
238
+ main();
239
+ }