Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
1198 lines
41 KiB
JavaScript
1198 lines
41 KiB
JavaScript
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
import assert from 'node:assert/strict';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { createHash } from 'node:crypto';
|
|
import {
|
|
chmodSync,
|
|
copyFileSync,
|
|
existsSync,
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { basename, dirname, join } from 'node:path';
|
|
import { describe, test } from 'node:test';
|
|
import {
|
|
createSourceCandidate,
|
|
controlledProcessEnvironment,
|
|
parseSha512File,
|
|
reproduceSourceCandidate,
|
|
signSourceCandidate,
|
|
sourceCandidateIdentity,
|
|
validateArchiveEntries,
|
|
validateGpgVerificationStatus,
|
|
validatePackageVersions,
|
|
verifySourceCandidate,
|
|
} from './asf-source-release.mjs';
|
|
|
|
describe('ASF source release identity', () => {
|
|
test('uses the incubating source distribution name for plain versions only', () => {
|
|
assert.deepEqual(sourceCandidateIdentity('0.1.12'), {
|
|
archiveName: 'apache-maka-0.1.12-incubating-src.tar.gz',
|
|
rootDirectory: 'apache-maka-0.1.12-incubating',
|
|
version: '0.1.12',
|
|
});
|
|
assert.throws(() => sourceCandidateIdentity('0.1.12-incubating'), /Invalid release version/);
|
|
});
|
|
});
|
|
|
|
describe('ASF source release verification', () => {
|
|
test('validates checksum identity and archive boundaries', () => {
|
|
const name = 'apache-maka-0.1.12-incubating-src.tar.gz';
|
|
assert.equal(parseSha512File(`${'a'.repeat(128)} ${name}\n`, name), 'a'.repeat(128));
|
|
assert.throws(() => parseSha512File(`${'a'.repeat(128)} another.tar.gz\n`, name), /expected/);
|
|
|
|
const root = 'apache-maka-0.1.12-incubating';
|
|
assert.doesNotThrow(() =>
|
|
validateArchiveEntries(
|
|
[
|
|
`${root}/`,
|
|
`${root}/DISCLAIMER-WIP`,
|
|
`${root}/LICENSE`,
|
|
`${root}/NOTICE`,
|
|
`${root}/package-lock.json`,
|
|
`${root}/package.json`,
|
|
`${root}/src/index.ts`,
|
|
],
|
|
root,
|
|
),
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateArchiveEntries(
|
|
[
|
|
`${root}/DISCLAIMER-WIP`,
|
|
`${root}/LICENSE`,
|
|
`${root}/NOTICE`,
|
|
`${root}/package-lock.json`,
|
|
`${root}/package.json`,
|
|
`${root}/.maka-shots/review.png`,
|
|
`${root}/node_modules/dependency/index.js`,
|
|
],
|
|
root,
|
|
),
|
|
/Forbidden archive entry/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateArchiveEntries(
|
|
[
|
|
`${root}/DISCLAIMER-WIP`,
|
|
`${root}/LICENSE`,
|
|
`${root}/NOTICE`,
|
|
`${root}/package-lock.json`,
|
|
`${root}/package.json`,
|
|
`${root}/fixtures/evil.bin`,
|
|
`${root}/fixtures/EVIL.BIN`,
|
|
],
|
|
root,
|
|
),
|
|
/Cross-platform archive entry collision/,
|
|
);
|
|
});
|
|
|
|
test('requires one current valid GPG signature status', () => {
|
|
const fingerprint = 'A'.repeat(40);
|
|
assert.deepEqual(
|
|
validateGpgVerificationStatus(
|
|
`[GNUPG:] GOODSIG ABCDEF0123456789 Release Test\n[GNUPG:] VALIDSIG ${fingerprint} 2026-08-20 1787193600 0 4 0 1 10 00 ${fingerprint}\n`,
|
|
),
|
|
{ fingerprint, hashAlgorithm: 10 },
|
|
);
|
|
for (const status of ['EXPKEYSIG', 'EXPSIG', 'KEYEXPIRED', 'KEYREVOKED', 'REVKEYSIG']) {
|
|
assert.throws(
|
|
() =>
|
|
validateGpgVerificationStatus(
|
|
`[GNUPG:] ${status} ABCDEF0123456789 Release Test\n[GNUPG:] GOODSIG ABCDEF0123456789 Release Test\n[GNUPG:] VALIDSIG ${fingerprint} 2026-08-20 1787193600 0 4 0 1 10 00 ${fingerprint}\n`,
|
|
),
|
|
new RegExp(status),
|
|
);
|
|
}
|
|
});
|
|
|
|
test('requires package and lockfile versions to share one identity', () => {
|
|
const packageJson = { version: '0.1.12' };
|
|
const packageLock = { packages: { '': { version: '0.1.12' } }, version: '0.1.12' };
|
|
assert.doesNotThrow(() =>
|
|
validatePackageVersions({ packageJson, packageLock, source: 'fixture', version: '0.1.12' }),
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validatePackageVersions({
|
|
packageJson,
|
|
packageLock: { ...packageLock, version: '9.9.9' },
|
|
source: 'fixture',
|
|
version: '0.1.12',
|
|
}),
|
|
/package-lock\.json.*9\.9\.9/,
|
|
);
|
|
});
|
|
|
|
test('rejects Category X dependencies from a nested package lockfile', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'tools/runtime/package.json': `${JSON.stringify({ name: 'runtime', private: true, license: 'Apache-2.0' })}\n`,
|
|
'tools/runtime/package-lock.json': `${JSON.stringify({
|
|
lockfileVersion: 3,
|
|
name: 'runtime',
|
|
packages: {
|
|
'': { name: 'runtime', license: 'Apache-2.0' },
|
|
'node_modules/category-x-runtime': {
|
|
version: '1.0.0',
|
|
license: 'LGPL-3.0-or-later',
|
|
},
|
|
},
|
|
})}\n`,
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
/Category X.*category-x-runtime.*LGPL-3\.0-or-later/,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('accepts a classified dependency from a nested package lockfile', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'tools/source/package.json': `${JSON.stringify({
|
|
dependencies: { 'source-helper': '1.0.0' },
|
|
name: 'source',
|
|
private: true,
|
|
})}\n`,
|
|
'tools/source/package-lock.json': `${JSON.stringify({
|
|
lockfileVersion: 3,
|
|
name: 'source',
|
|
packages: {
|
|
'': { name: 'source', license: 'Apache-2.0' },
|
|
'node_modules/source-helper': { version: '1.0.0', license: 'MIT' },
|
|
},
|
|
})}\n`,
|
|
});
|
|
try {
|
|
await assert.doesNotReject(() => verifySourceCandidate({ archivePath: fixture.archivePath }));
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('rejects an unclassified dependency from a nested package lockfile', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'tools/unknown/package.json': `${JSON.stringify({ name: 'unknown', private: true, license: 'Apache-2.0' })}\n`,
|
|
'tools/unknown/package-lock.json': `${JSON.stringify({
|
|
lockfileVersion: 3,
|
|
name: 'unknown',
|
|
packages: {
|
|
'': { name: 'unknown', license: 'Apache-2.0' },
|
|
'node_modules/unknown-runtime': { version: '1.0.0' },
|
|
},
|
|
})}\n`,
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
/Cannot safely classify unknown-runtime@1\.0\.0.*tools\/unknown\/package-lock\.json/,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('accepts an unlicensed private manifest without dependencies', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'tools/profile/package.json': `${JSON.stringify({ name: 'profile', private: true })}\n`,
|
|
});
|
|
try {
|
|
await assert.doesNotReject(() => verifySourceCandidate({ archivePath: fixture.archivePath }));
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('rejects unlicensed private manifests whose dependencies lack lock provenance', async () => {
|
|
const dependencyDeclarations = {
|
|
bundleDependencies: ['runtime-helper'],
|
|
bundledDependencies: ['runtime-helper'],
|
|
dependencies: { 'runtime-helper': '1.0.0' },
|
|
devDependencies: { 'runtime-helper': '1.0.0' },
|
|
optionalDependencies: { 'runtime-helper': '1.0.0' },
|
|
peerDependencies: { 'runtime-helper': '1.0.0' },
|
|
};
|
|
for (const [field, declaration] of Object.entries(dependencyDeclarations)) {
|
|
const fixture = createFixtureCandidate({
|
|
[`tools/${field}/package.json`]: `${JSON.stringify({
|
|
[field]: declaration,
|
|
name: field,
|
|
private: true,
|
|
})}\n`,
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
new RegExp(
|
|
`Cannot safely classify.*${field}.*without (?:matching )?lock provenance`,
|
|
'u',
|
|
),
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
}
|
|
});
|
|
|
|
test('does not accept arbitrary notice files as dependency license authority', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'tools/runtime/THIRD_PARTY_NOTICES.txt':
|
|
'Package: unknown-runtime@1.0.0\nSelected license: MIT\n',
|
|
'tools/runtime/package-lock.json': `${JSON.stringify({
|
|
lockfileVersion: 3,
|
|
packages: {
|
|
'': { name: 'runtime' },
|
|
'node_modules/unknown-runtime': { version: '1.0.0' },
|
|
},
|
|
})}\n`,
|
|
'tools/runtime/package.json': `${JSON.stringify({
|
|
dependencies: { 'unknown-runtime': '1.0.0' },
|
|
license: 'Apache-2.0',
|
|
name: 'runtime',
|
|
private: true,
|
|
})}\n`,
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
/Cannot safely classify unknown-runtime@1\.0\.0/,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('uses the generated npm notice as the dependency license authority', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt':
|
|
'Package: source-helper@1.0.0\nSelected license: MIT\n',
|
|
'tools/source/package-lock.json': `${JSON.stringify({
|
|
lockfileVersion: 3,
|
|
packages: {
|
|
'': { name: 'source' },
|
|
'node_modules/source-helper': { version: '1.0.0' },
|
|
},
|
|
})}\n`,
|
|
'tools/source/package.json': `${JSON.stringify({
|
|
dependencies: { 'source-helper': '1.0.0' },
|
|
name: 'source',
|
|
private: true,
|
|
})}\n`,
|
|
});
|
|
try {
|
|
await assert.doesNotReject(() => verifySourceCandidate({ archivePath: fixture.archivePath }));
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('does not let lock metadata override a Category X generated notice', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt':
|
|
'Package: disguised-runtime@1.0.0\nSelected license: GPL-3.0-only\n',
|
|
'tools/runtime/package-lock.json': `${JSON.stringify({
|
|
lockfileVersion: 3,
|
|
packages: {
|
|
'': { name: 'runtime' },
|
|
'node_modules/disguised-runtime': { license: 'MIT', version: '1.0.0' },
|
|
},
|
|
})}\n`,
|
|
'tools/runtime/package.json': `${JSON.stringify({
|
|
dependencies: { 'disguised-runtime': '1.0.0' },
|
|
license: 'Apache-2.0',
|
|
name: 'runtime',
|
|
private: true,
|
|
})}\n`,
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
/Category X.*disguised-runtime.*GPL-3\.0-only/,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('rejects lock links without a candidate-owned workspace target', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'tools/runtime/package-lock.json': `${JSON.stringify({
|
|
lockfileVersion: 3,
|
|
packages: {
|
|
'': { name: 'runtime' },
|
|
'node_modules/evil-runtime': { link: true, resolved: '../../evil-runtime' },
|
|
},
|
|
})}\n`,
|
|
'tools/runtime/package.json': `${JSON.stringify({ license: 'Apache-2.0', name: 'runtime' })}\n`,
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
/Cannot safely classify workspace link.*evil-runtime/,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('rejects Category X metadata on workspace package lock entries', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'packages/evil/package.json': `${JSON.stringify({
|
|
license: 'Apache-2.0',
|
|
name: 'evil',
|
|
version: '1.0.0',
|
|
})}\n`,
|
|
'package-lock.json': `${JSON.stringify({
|
|
lockfileVersion: 3,
|
|
name: 'maka',
|
|
packages: {
|
|
'': { name: 'maka', version: '0.1.12' },
|
|
'packages/evil': { license: 'GPL-3.0-only', name: 'evil', version: '1.0.0' },
|
|
},
|
|
version: '0.1.12',
|
|
})}\n`,
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
/Category X.*packages\/evil.*GPL-3\.0-only/,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('binds licensed manifest dependencies to a package lock closure', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'tools/runtime/package.json': `${JSON.stringify({
|
|
dependencies: { 'unlocked-runtime': '1.0.0' },
|
|
license: 'Apache-2.0',
|
|
name: 'runtime',
|
|
private: true,
|
|
})}\n`,
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
/Cannot safely classify.*tools\/runtime\/package\.json.*without (?:matching )?lock provenance/,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('rejects unsupported npm lockfile versions', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'tools/runtime/package-lock.json': `${JSON.stringify({
|
|
lockfileVersion: 99,
|
|
packages: { '': { name: 'runtime' } },
|
|
})}\n`,
|
|
'tools/runtime/package.json': `${JSON.stringify({ license: 'Apache-2.0', name: 'runtime' })}\n`,
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
/Unsupported npm lockfile version 99/,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('classifies dependencies from a nested npm shrinkwrap', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'tools/source/npm-shrinkwrap.json': `${JSON.stringify({
|
|
lockfileVersion: 3,
|
|
packages: {
|
|
'': { name: 'source' },
|
|
'node_modules/source-helper': { license: 'MIT', version: '1.0.0' },
|
|
},
|
|
})}\n`,
|
|
'tools/source/package.json': `${JSON.stringify({
|
|
dependencies: { 'source-helper': '1.0.0' },
|
|
name: 'source',
|
|
private: true,
|
|
})}\n`,
|
|
});
|
|
try {
|
|
await assert.doesNotReject(() => verifySourceCandidate({ archivePath: fixture.archivePath }));
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('rejects unsupported nested package manager lockfiles', async () => {
|
|
for (const [name, contents] of [
|
|
['pnpm-lock.yaml', 'lockfileVersion: 9\n'],
|
|
['yarn.lock', '# yarn lockfile v1\n'],
|
|
]) {
|
|
const fixture = createFixtureCandidate({ [`tools/source/${name}`]: contents });
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
/Cannot safely classify unsupported package lockfile/,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
}
|
|
});
|
|
|
|
test('rejects Category X and unknown nested package manifest licenses', async () => {
|
|
for (const [license, expected] of [
|
|
['AGPL-3.0-only', /Category X/],
|
|
['LicenseRef-Unknown', /Cannot safely classify license/],
|
|
]) {
|
|
const fixture = createFixtureCandidate({
|
|
'tools/source/package.json': `${JSON.stringify({ license, name: 'source' })}\n`,
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
expected,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
}
|
|
});
|
|
|
|
test('rejects compiled artifacts regardless of their file extension', async () => {
|
|
const formats = new Map([
|
|
['ELF', Buffer.from([0x7f, 0x45, 0x4c, 0x46])],
|
|
['Mach-O', Buffer.from([0xfe, 0xed, 0xfa, 0xcf])],
|
|
['Mach-O fat little-endian', Buffer.from([0xbe, 0xba, 0xfe, 0xca])],
|
|
['Mach-O fat64 little-endian', Buffer.from([0xbf, 0xba, 0xfe, 0xca])],
|
|
['PE', Buffer.from('MZ')],
|
|
['WASM', Buffer.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00])],
|
|
['ZIP/JAR', Buffer.from([0x50, 0x4b, 0x03, 0x04])],
|
|
['thin ar archive', Buffer.from('!<thin>\n')],
|
|
]);
|
|
for (const [format, bytes] of formats) {
|
|
const fixture = createFixtureCandidate({
|
|
'docs/code-origin-audit.md':
|
|
'### Source archive non-text inventory\n\n- `fixtures/runtime.dat`: a claimed fixture.\n',
|
|
'fixtures/runtime.dat': bytes,
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
new RegExp(`Compiled artifact.*fixtures/runtime\\.dat.*${format.replace('/', '\\/')}`),
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
}
|
|
});
|
|
|
|
test('rejects ZIP payloads hidden behind an allowed source prefix', async () => {
|
|
for (const bytes of [
|
|
Buffer.concat([Buffer.from('source preface\n'), Buffer.from([0x50, 0x4b, 0x03, 0x04])]),
|
|
Buffer.concat([
|
|
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
Buffer.from([0x50, 0x4b, 0x03, 0x04]),
|
|
]),
|
|
]) {
|
|
const fixture = createFixtureCandidate({
|
|
'docs/code-origin-audit.md':
|
|
'### Source archive non-text inventory\n\n- `fixtures/runtime.dat`: a claimed fixture.\n',
|
|
'fixtures/runtime.dat': bytes,
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
/forbidden ZIP\/JAR content/,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
}
|
|
});
|
|
|
|
test('accepts an inventoried source image', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'docs/code-origin-audit.md':
|
|
'### Source archive non-text inventory\n\n- `fixtures/*.png`: test-generated source images.\n',
|
|
'fixtures/source.png': Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
});
|
|
try {
|
|
await assert.doesNotReject(() => verifySourceCandidate({ archivePath: fixture.archivePath }));
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('accepts an inventoried non-text source fixture', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'docs/code-origin-audit.md':
|
|
'### Source archive non-text inventory\n\n- `fixtures/control.ts`: a raw control-character fixture.\n',
|
|
'fixtures/control.ts': Buffer.from([0x63, 0x6f, 0x6e, 0x73, 0x74, 0x20, 0x78, 0x00]),
|
|
});
|
|
try {
|
|
await assert.doesNotReject(() => verifySourceCandidate({ archivePath: fixture.archivePath }));
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('rejects unknown non-text release inputs', async () => {
|
|
const fixture = createFixtureCandidate({ 'fixtures/unknown.bin': Buffer.from([0x01, 0x02]) });
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
/Cannot safely classify non-text release input.*fixtures\/unknown\.bin/,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('rejects unknown binary paths even when their bytes are valid UTF-8', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'fixtures/unknown.bin': Buffer.from('printable but still an unknown binary input'),
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
/Cannot safely classify non-text release input.*fixtures\/unknown\.bin/,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('keeps provenance inventory entries inside their Markdown section', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'docs/code-origin-audit.md':
|
|
'### Source archive non-text inventory\n\n## Another section\n\n- `fixtures/unknown.bin`: outside the inventory.\n',
|
|
'fixtures/unknown.bin': Buffer.from([0x01, 0x02]),
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
/Cannot safely classify non-text release input.*fixtures\/unknown\.bin/,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('rejects provenance patterns without a fixed path prefix', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'docs/code-origin-audit.md':
|
|
'### Source archive non-text inventory\n\n- `**`: overbroad inventory.\n',
|
|
'fixtures/unknown.bin': Buffer.from([0x01, 0x02]),
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: fixture.archivePath }),
|
|
/Unsafe source archive inventory pattern/,
|
|
);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('allows a double-star inventory to match zero nested directories', async () => {
|
|
const fixture = createFixtureCandidate({
|
|
'docs/code-origin-audit.md':
|
|
'### Source archive non-text inventory\n\n- `docs/images/**/*.png`: reviewed screenshots.\n',
|
|
'docs/images/root.png': Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
});
|
|
try {
|
|
await assert.doesNotReject(() => verifySourceCandidate({ archivePath: fixture.archivePath }));
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test('filters controlled environment names case-insensitively', async () => {
|
|
await withEnvironmentVariables(
|
|
{ gIt_Prefix_Probe: 'prefix', mAkA_Exact_Probe: 'exact' },
|
|
() => {
|
|
const environment = controlledProcessEnvironment({
|
|
excludedNames: ['MAKA_EXACT_PROBE'],
|
|
excludedPrefixes: ['GIT_'],
|
|
overrides: { GIT_CONFIG_NOSYSTEM: '1' },
|
|
});
|
|
const names = Object.keys(environment).map((name) => name.toUpperCase());
|
|
assert.equal(names.includes('MAKA_EXACT_PROBE'), false);
|
|
assert.equal(names.includes('GIT_PREFIX_PROBE'), false);
|
|
assert.equal(environment.GIT_CONFIG_NOSYSTEM, '1');
|
|
},
|
|
);
|
|
});
|
|
|
|
test('requires a signature before reading unauthenticated candidate metadata', async () => {
|
|
const temporaryRoot = mkdtempSync(join(tmpdir(), 'maka-asf-auth-order-test-'));
|
|
const identity = sourceCandidateIdentity('0.1.12');
|
|
const archivePath = join(temporaryRoot, identity.archiveName);
|
|
try {
|
|
writeFileSync(archivePath, 'not a tar archive\n');
|
|
const keysPath = join(temporaryRoot, 'KEYS');
|
|
writeFileSync(keysPath, '');
|
|
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath, keysPath }),
|
|
/Detached signature does not exist/,
|
|
);
|
|
} finally {
|
|
rmSync(temporaryRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
test('documents the checkout-only DeepSeek Harness toolchain build in the candidate', async () => {
|
|
const temporaryRoot = mkdtempSync(join(tmpdir(), 'maka-asf-dsh-contract-test-'));
|
|
const repositoryRoot = join(temporaryRoot, 'repository');
|
|
const outputDirectory = join(temporaryRoot, 'release');
|
|
const identity = sourceCandidateIdentity('0.1.12');
|
|
try {
|
|
writeReleaseContents(repositoryRoot, { includeAttributes: true });
|
|
mkdirSync(join(repositoryRoot, 'packages/eval/harbor/deepseek-harness-toolchain'), {
|
|
recursive: true,
|
|
});
|
|
mkdirSync(join(repositoryRoot, 'packages/eval/harbor/deepseek-harness-profile'), {
|
|
recursive: true,
|
|
});
|
|
mkdirSync(join(repositoryRoot, 'scripts'), { recursive: true });
|
|
copyFileSync(
|
|
join(import.meta.dirname, '../packages/eval/README.md'),
|
|
join(repositoryRoot, 'packages/eval/README.md'),
|
|
);
|
|
writeFileSync(
|
|
join(repositoryRoot, 'packages/eval/harbor/deepseek-harness-toolchain/package.json'),
|
|
'{}\n',
|
|
);
|
|
writeFileSync(
|
|
join(repositoryRoot, 'packages/eval/harbor/deepseek-harness-toolchain/package-lock.json'),
|
|
'{}\n',
|
|
);
|
|
writeFileSync(
|
|
join(repositoryRoot, 'scripts/prepare-deepseek-harness-toolchain.mjs'),
|
|
'export {};\n',
|
|
);
|
|
for (const name of ['package.json', 'cordis.patch.yml']) {
|
|
copyFileSync(
|
|
join(import.meta.dirname, `../packages/eval/harbor/deepseek-harness-profile/${name}`),
|
|
join(repositoryRoot, `packages/eval/harbor/deepseek-harness-profile/${name}`),
|
|
);
|
|
}
|
|
|
|
git(repositoryRoot, ['init']);
|
|
git(repositoryRoot, ['add', '.']);
|
|
commitFixture(repositoryRoot, 'test DSH source archive contract');
|
|
|
|
const candidate = await createSourceCandidate({
|
|
outputDirectory,
|
|
repositoryRoot,
|
|
version: '0.1.12',
|
|
});
|
|
const entries = execFileSync('tar', ['-tzf', candidate.archivePath], { encoding: 'utf8' });
|
|
const archivedReadme = execFileSync(
|
|
'tar',
|
|
['-xOzf', candidate.archivePath, `${identity.rootDirectory}/packages/eval/README.md`],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
|
|
assert.doesNotMatch(entries, /deepseek-harness-toolchain\/package(?:-lock)?\.json/);
|
|
assert.match(entries, /scripts\/prepare-deepseek-harness-toolchain\.mjs/);
|
|
for (const name of ['package.json', 'cordis.patch.yml']) {
|
|
const relativePath = `packages/eval/harbor/deepseek-harness-profile/${name}`;
|
|
assert.deepEqual(
|
|
execFileSync('tar', [
|
|
'-xOzf',
|
|
candidate.archivePath,
|
|
`${identity.rootDirectory}/${relativePath}`,
|
|
]),
|
|
readFileSync(join(repositoryRoot, relativePath)),
|
|
);
|
|
}
|
|
assert.match(archivedReadme, /only from a complete Git checkout/u);
|
|
assert.match(archivedReadme, /intentionally excluded from ASF source archives/u);
|
|
} finally {
|
|
rmSync(temporaryRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
test('creates reproducible candidates from committed files only', async () => {
|
|
const temporaryRoot = mkdtempSync(join(tmpdir(), 'maka-asf-source-test-'));
|
|
const repositoryRoot = join(temporaryRoot, 'repository');
|
|
const firstOutput = join(temporaryRoot, 'first');
|
|
const hostileOutput = join(temporaryRoot, 'hostile');
|
|
mkdirSync(repositoryRoot, { recursive: true });
|
|
try {
|
|
writeReleaseContents(repositoryRoot, { includeAttributes: true });
|
|
writeFileSync(join(repositoryRoot, '.gitignore'), 'untracked.txt\n');
|
|
writeFileSync(join(repositoryRoot, 'README.md'), 'release fixture\n'.repeat(4096));
|
|
writeFileSync(join(repositoryRoot, 'untracked.txt'), 'must not be released\n');
|
|
mkdirSync(join(repositoryRoot, '.claude'));
|
|
mkdirSync(join(repositoryRoot, '.maka-shots'));
|
|
writeFileSync(join(repositoryRoot, '.claude/launch.json'), '{}\n');
|
|
writeFileSync(join(repositoryRoot, '.maka-shots/review.png'), 'review evidence\n');
|
|
writeFileSync(join(repositoryRoot, 'maka-proposal-zh-review.txt'), 'working notes\n');
|
|
mkdirSync(join(repositoryRoot, 'packages/eval/harbor/deepseek-harness-toolchain'), {
|
|
recursive: true,
|
|
});
|
|
mkdirSync(join(repositoryRoot, 'packages/eval/harbor/deepseek-harness-profile'), {
|
|
recursive: true,
|
|
});
|
|
writeFileSync(
|
|
join(repositoryRoot, 'packages/eval/harbor/deepseek-harness-toolchain/package-lock.json'),
|
|
'{}\n',
|
|
);
|
|
writeFileSync(
|
|
join(repositoryRoot, 'packages/eval/harbor/deepseek-harness-toolchain/package.json'),
|
|
'{}\n',
|
|
);
|
|
writeFileSync(
|
|
join(
|
|
repositoryRoot,
|
|
'packages/eval/harbor/deepseek-harness-toolchain/patch-subprocess-local.mjs',
|
|
),
|
|
'export {};\n',
|
|
);
|
|
writeFileSync(
|
|
join(repositoryRoot, 'packages/eval/harbor/deepseek-harness-profile/cordis.yml'),
|
|
'profile: maka-eval\n',
|
|
);
|
|
|
|
git(repositoryRoot, ['init']);
|
|
git(repositoryRoot, [
|
|
'add',
|
|
'package.json',
|
|
'package-lock.json',
|
|
'DISCLAIMER-WIP',
|
|
'LICENSE',
|
|
'NOTICE',
|
|
'.gitignore',
|
|
'.gitattributes',
|
|
'README.md',
|
|
'.claude/launch.json',
|
|
'.maka-shots/review.png',
|
|
'maka-proposal-zh-review.txt',
|
|
'packages/eval/harbor/deepseek-harness-toolchain/package-lock.json',
|
|
'packages/eval/harbor/deepseek-harness-toolchain/package.json',
|
|
'packages/eval/harbor/deepseek-harness-toolchain/patch-subprocess-local.mjs',
|
|
'packages/eval/harbor/deepseek-harness-profile/cordis.yml',
|
|
]);
|
|
git(repositoryRoot, [
|
|
'-c',
|
|
'user.name=ASF Release Test',
|
|
'-c',
|
|
'user.email=release-test@example.invalid',
|
|
'commit',
|
|
'-m',
|
|
'test fixture',
|
|
]);
|
|
|
|
const first = await createSourceCandidate({
|
|
outputDirectory: firstOutput,
|
|
repositoryRoot,
|
|
version: '0.1.12',
|
|
});
|
|
await assert.rejects(
|
|
() =>
|
|
createSourceCandidate({
|
|
outputDirectory: firstOutput,
|
|
repositoryRoot,
|
|
version: '0.1.12',
|
|
}),
|
|
/Refusing to overwrite existing release output/,
|
|
);
|
|
git(repositoryRoot, ['config', 'tar.umask', '0077']);
|
|
writeFileSync(join(repositoryRoot, '.git/info/attributes'), 'README.md export-ignore\n');
|
|
|
|
const ambientTemplate = join(temporaryRoot, 'ambient-template');
|
|
mkdirSync(join(ambientTemplate, 'info'), { recursive: true });
|
|
writeFileSync(join(ambientTemplate, 'info/attributes'), 'README.md export-ignore\n');
|
|
|
|
const ambientRepository = join(temporaryRoot, 'ambient-repository');
|
|
git(temporaryRoot, ['clone', '--quiet', repositoryRoot, ambientRepository]);
|
|
writeFileSync(join(ambientRepository, 'README.md'), 'ambient repository\n');
|
|
git(ambientRepository, ['add', 'README.md']);
|
|
git(ambientRepository, [
|
|
'-c',
|
|
'user.name=ASF Release Test',
|
|
'-c',
|
|
'user.email=release-test@example.invalid',
|
|
'commit',
|
|
'-m',
|
|
'change ambient repository',
|
|
]);
|
|
const hostile = await withEnvironmentVariables(
|
|
{
|
|
GIT_DIR: join(ambientRepository, '.git'),
|
|
GIT_TEMPLATE_DIR: ambientTemplate,
|
|
GZIP: '-l',
|
|
},
|
|
() =>
|
|
createSourceCandidate({
|
|
outputDirectory: hostileOutput,
|
|
repositoryRoot,
|
|
version: '0.1.12',
|
|
}),
|
|
);
|
|
assert.deepEqual(readFileSync(first.archivePath), readFileSync(hostile.archivePath));
|
|
assert.equal(hostile.commit, first.commit);
|
|
assert.deepEqual(
|
|
readFileSync(`${first.archivePath}.sha512`),
|
|
readFileSync(`${hostile.archivePath}.sha512`),
|
|
);
|
|
await assert.doesNotReject(() => verifySourceCandidate({ archivePath: first.archivePath }));
|
|
|
|
const entries = execFileSync('tar', ['-tzf', basename(first.archivePath)], {
|
|
cwd: dirname(first.archivePath),
|
|
encoding: 'utf8',
|
|
});
|
|
assert.doesNotMatch(entries, /untracked\.txt/);
|
|
assert.doesNotMatch(entries, /\.claude|\.maka-shots|maka-proposal-zh-review/);
|
|
assert.doesNotMatch(entries, /deepseek-harness-toolchain\/package(?:-lock)?\.json/);
|
|
assert.match(entries, /deepseek-harness-toolchain\/patch-subprocess-local\.mjs/);
|
|
assert.match(entries, /deepseek-harness-profile\/cordis\.yml/);
|
|
assert.match(entries, /README\.md/);
|
|
|
|
const originalCompressedBytes = readFileSync(first.archivePath);
|
|
rewriteArchiveCompression(first.archivePath, 1);
|
|
assert.notDeepEqual(readFileSync(first.archivePath), originalCompressedBytes);
|
|
await assert.doesNotReject(() =>
|
|
reproduceSourceCandidate({
|
|
archivePath: first.archivePath,
|
|
repositoryRoot,
|
|
revision: first.commit,
|
|
}),
|
|
);
|
|
writeFileSync(join(repositoryRoot, 'README.md'), 'different committed payload\n');
|
|
git(repositoryRoot, ['add', 'README.md']);
|
|
commitFixture(repositoryRoot, 'change candidate payload');
|
|
await assert.rejects(
|
|
() =>
|
|
reproduceSourceCandidate({
|
|
archivePath: first.archivePath,
|
|
repositoryRoot,
|
|
revision: 'HEAD',
|
|
}),
|
|
/Candidate source payload does not match/,
|
|
);
|
|
|
|
const gpgHome = join(temporaryRoot, 'gnupg');
|
|
const keysPath = join(temporaryRoot, 'KEYS');
|
|
const fingerprint = generateSigningKey({
|
|
algorithm: 'rsa2048',
|
|
gpgHome,
|
|
identity: 'ASF Release Test <release-test@example.invalid>',
|
|
signingSubkeyAlgorithm: 'rsa2048',
|
|
usage: 'cert',
|
|
});
|
|
writeFileSync(join(gpgHome, 'gpg.conf'), 'digest-algo SHA1\n');
|
|
await assert.doesNotReject(() =>
|
|
signSourceCandidate({
|
|
archivePath: first.archivePath,
|
|
gpgHome,
|
|
keyFingerprint: fingerprint,
|
|
repositoryRoot,
|
|
revision: first.commit,
|
|
}),
|
|
);
|
|
exportPublicKey({ fingerprint, gpgHome, keysPath });
|
|
await assert.doesNotReject(() =>
|
|
verifySourceCandidate({ archivePath: first.archivePath, keysPath }),
|
|
);
|
|
|
|
rmSync(`${first.archivePath}.asc`);
|
|
execFileSync(
|
|
'gpg',
|
|
[
|
|
'--batch',
|
|
'--homedir',
|
|
gpgHome,
|
|
'--digest-algo',
|
|
'SHA512',
|
|
'--detach-sign',
|
|
'--output',
|
|
`${first.archivePath}.asc`,
|
|
first.archivePath,
|
|
],
|
|
{ stdio: 'ignore' },
|
|
);
|
|
const checksum = readFileSync(`${first.archivePath}.sha512`);
|
|
writeFileSync(`${first.archivePath}.sha512`, 'not a checksum\n');
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: first.archivePath, keysPath }),
|
|
/ASCII-armored/,
|
|
);
|
|
writeFileSync(`${first.archivePath}.sha512`, checksum);
|
|
|
|
rmSync(`${first.archivePath}.asc`);
|
|
execFileSync(
|
|
'gpg',
|
|
[
|
|
'--batch',
|
|
'--homedir',
|
|
gpgHome,
|
|
'--armor',
|
|
'--digest-algo',
|
|
'SHA1',
|
|
'--detach-sign',
|
|
'--output',
|
|
`${first.archivePath}.asc`,
|
|
first.archivePath,
|
|
],
|
|
{ stdio: 'ignore' },
|
|
);
|
|
await assert.rejects(
|
|
() => verifySourceCandidate({ archivePath: first.archivePath, keysPath }),
|
|
/must use SHA-256, SHA-384, or SHA-512/,
|
|
);
|
|
|
|
rmSync(`${first.archivePath}.asc`);
|
|
// Keep the homedir short enough for GPG agent socket paths on macOS.
|
|
const ed25519Home = join(temporaryRoot, 'ed');
|
|
const ed25519Fingerprint = generateSigningKey({
|
|
algorithm: 'ed25519',
|
|
gpgHome: ed25519Home,
|
|
identity: 'ASF Ed25519 Release Test <release-test@example.invalid>',
|
|
});
|
|
await assert.rejects(
|
|
() =>
|
|
signSourceCandidate({
|
|
archivePath: first.archivePath,
|
|
gpgHome: ed25519Home,
|
|
keyFingerprint: ed25519Fingerprint,
|
|
repositoryRoot,
|
|
revision: first.commit,
|
|
}),
|
|
/must be RSA with at least 2048 bits/,
|
|
);
|
|
assert.equal(existsSync(`${first.archivePath}.asc`), false);
|
|
|
|
const rsa1024Home = join(temporaryRoot, 'r');
|
|
const rsa1024Fingerprint = generateSigningKey({
|
|
algorithm: 'rsa2048',
|
|
gpgHome: rsa1024Home,
|
|
identity: 'ASF RSA-1024 Subkey Test <release-test@example.invalid>',
|
|
signingSubkeyAlgorithm: 'rsa1024',
|
|
usage: 'cert',
|
|
});
|
|
await assert.rejects(
|
|
() =>
|
|
signSourceCandidate({
|
|
archivePath: first.archivePath,
|
|
gpgHome: rsa1024Home,
|
|
keyFingerprint: rsa1024Fingerprint,
|
|
repositoryRoot,
|
|
revision: first.commit,
|
|
}),
|
|
/must be RSA with at least 2048 bits/,
|
|
);
|
|
assert.equal(existsSync(`${first.archivePath}.asc`), false);
|
|
} finally {
|
|
rmSync(temporaryRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
test('does not let ambient tar options control archive verification', async () => {
|
|
const temporaryRoot = mkdtempSync(join(tmpdir(), 'maka-asf-tar-options-test-'));
|
|
const identity = sourceCandidateIdentity('0.1.12');
|
|
const sourceRoot = join(temporaryRoot, identity.rootDirectory);
|
|
const archivePath = join(temporaryRoot, identity.archiveName);
|
|
try {
|
|
writeReleaseContents(sourceRoot);
|
|
mkdirSync(join(sourceRoot, '.agents'), { recursive: true });
|
|
writeFileSync(join(sourceRoot, '.agents/secret.txt'), 'must not be released\n');
|
|
execFileSync('tar', ['-czf', archivePath, identity.rootDirectory], {
|
|
cwd: temporaryRoot,
|
|
stdio: 'ignore',
|
|
});
|
|
const digest = createHash('sha512').update(readFileSync(archivePath)).digest('hex');
|
|
writeFileSync(`${archivePath}.sha512`, `${digest} ${identity.archiveName}\n`);
|
|
|
|
for (const [name, value] of Object.entries({
|
|
GZIP: '-l',
|
|
TAR_OPTIONS: `--exclude=${identity.rootDirectory}/.agents`,
|
|
TAR_READER_OPTIONS: 'tar:hdrcharset=BOGUS',
|
|
})) {
|
|
await withEnvironmentVariables({ [name]: value }, () =>
|
|
assert.rejects(() => verifySourceCandidate({ archivePath }), /Forbidden archive entry/),
|
|
);
|
|
}
|
|
} finally {
|
|
rmSync(temporaryRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
function git(repositoryRoot, arguments_) {
|
|
execFileSync('git', arguments_, { cwd: repositoryRoot, stdio: 'ignore' });
|
|
}
|
|
|
|
function writeReleaseContents(root, { includeAttributes = false } = {}) {
|
|
mkdirSync(root, { recursive: true });
|
|
writeFileSync(
|
|
join(root, 'package.json'),
|
|
`${JSON.stringify({ license: 'Apache-2.0', name: 'maka', private: true, version: '0.1.12' }, null, 2)}\n`,
|
|
);
|
|
writeFileSync(
|
|
join(root, 'package-lock.json'),
|
|
`${JSON.stringify({ lockfileVersion: 3, name: 'maka', packages: { '': { name: 'maka', version: '0.1.12' } }, version: '0.1.12' }, null, 2)}\n`,
|
|
);
|
|
writeFileSync(
|
|
join(root, 'DISCLAIMER-WIP'),
|
|
'Apache Maka is undergoing incubation at The Apache Software Foundation.\n',
|
|
);
|
|
writeFileSync(join(root, 'LICENSE'), 'Apache License, Version 2.0\n');
|
|
writeFileSync(join(root, 'NOTICE'), 'Apache Maka\n');
|
|
if (includeAttributes) {
|
|
copyFileSync(join(import.meta.dirname, '../.gitattributes'), join(root, '.gitattributes'));
|
|
}
|
|
}
|
|
|
|
function createFixtureCandidate(files = {}) {
|
|
const temporaryRoot = mkdtempSync(join(tmpdir(), 'maka-asf-candidate-test-'));
|
|
const identity = sourceCandidateIdentity('0.1.12');
|
|
const sourceRoot = join(temporaryRoot, identity.rootDirectory);
|
|
const archivePath = join(temporaryRoot, identity.archiveName);
|
|
writeReleaseContents(sourceRoot);
|
|
for (const [relativePath, contents] of Object.entries(files)) {
|
|
const path = join(sourceRoot, relativePath);
|
|
mkdirSync(dirname(path), { recursive: true });
|
|
writeFileSync(path, contents);
|
|
}
|
|
execFileSync('tar', ['-czf', archivePath, identity.rootDirectory], {
|
|
cwd: temporaryRoot,
|
|
stdio: 'ignore',
|
|
});
|
|
const digest = createHash('sha512').update(readFileSync(archivePath)).digest('hex');
|
|
writeFileSync(`${archivePath}.sha512`, `${digest} ${identity.archiveName}\n`);
|
|
return {
|
|
archivePath,
|
|
cleanup: () => rmSync(temporaryRoot, { force: true, recursive: true }),
|
|
};
|
|
}
|
|
|
|
function commitFixture(repositoryRoot, message) {
|
|
git(repositoryRoot, [
|
|
'-c',
|
|
'user.name=ASF Release Test',
|
|
'-c',
|
|
'user.email=release-test@example.invalid',
|
|
'commit',
|
|
'-m',
|
|
message,
|
|
]);
|
|
}
|
|
|
|
async function withEnvironmentVariables(values, callback) {
|
|
const previous = new Map(Object.keys(values).map((name) => [name, process.env[name]]));
|
|
Object.assign(process.env, values);
|
|
try {
|
|
return await callback();
|
|
} finally {
|
|
for (const [name, value] of previous) {
|
|
if (value === undefined) delete process.env[name];
|
|
else process.env[name] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
function rewriteArchiveCompression(archivePath, level) {
|
|
const tarPath = archivePath.slice(0, -3);
|
|
execFileSync('gzip', ['-d', archivePath], { stdio: 'ignore' });
|
|
execFileSync('gzip', ['-n', `-${level}`, tarPath], { stdio: 'ignore' });
|
|
const digest = createHash('sha512').update(readFileSync(archivePath)).digest('hex');
|
|
writeFileSync(`${archivePath}.sha512`, `${digest} ${basename(archivePath)}\n`);
|
|
}
|
|
|
|
function generateSigningKey({
|
|
algorithm,
|
|
gpgHome,
|
|
identity,
|
|
signingSubkeyAlgorithm,
|
|
usage = 'sign',
|
|
}) {
|
|
mkdirSync(gpgHome, { mode: 0o700 });
|
|
chmodSync(gpgHome, 0o700);
|
|
execFileSync(
|
|
'gpg',
|
|
[
|
|
'--batch',
|
|
'--homedir',
|
|
gpgHome,
|
|
'--pinentry-mode',
|
|
'loopback',
|
|
'--passphrase',
|
|
'',
|
|
'--quick-generate-key',
|
|
identity,
|
|
algorithm,
|
|
usage,
|
|
'1d',
|
|
],
|
|
{ stdio: 'ignore' },
|
|
);
|
|
const fingerprint = execFileSync(
|
|
'gpg',
|
|
['--batch', '--homedir', gpgHome, '--with-colons', '--fingerprint', '--list-secret-keys'],
|
|
{ encoding: 'utf8' },
|
|
)
|
|
.split(/\r?\n/)
|
|
.find((line) => line.startsWith('fpr:'))
|
|
?.split(':')[9];
|
|
assert.match(fingerprint, /^[0-9A-F]{40}$/);
|
|
if (signingSubkeyAlgorithm) {
|
|
execFileSync(
|
|
'gpg',
|
|
[
|
|
'--batch',
|
|
'--homedir',
|
|
gpgHome,
|
|
'--pinentry-mode',
|
|
'loopback',
|
|
'--passphrase',
|
|
'',
|
|
'--quick-add-key',
|
|
fingerprint,
|
|
signingSubkeyAlgorithm,
|
|
'sign',
|
|
'1d',
|
|
],
|
|
{ stdio: 'ignore' },
|
|
);
|
|
}
|
|
return fingerprint;
|
|
}
|
|
|
|
function exportPublicKey({ fingerprint, gpgHome, keysPath }) {
|
|
execFileSync(
|
|
'gpg',
|
|
['--batch', '--homedir', gpgHome, '--armor', '--output', keysPath, '--export', fingerprint],
|
|
{ stdio: 'ignore' },
|
|
);
|
|
}
|