Compare commits
17 commits
build/62-p
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 92f2eb3949 | |||
|
|
37e6a2ad5a | ||
|
|
82494e94fd | ||
| 33d583892e | |||
|
|
4333ce63bf | ||
|
|
5f2f58a249 | ||
|
|
1bb4bd608c | ||
|
|
721ba343cc | ||
|
|
44bbeadff6 | ||
|
|
cef903b77e | ||
|
|
9404c09cae | ||
| 74e52b4210 | |||
| c4110e7f7e | |||
|
|
1335232002 | ||
|
|
bf84b19a07 | ||
|
|
aedce42c56 | ||
|
|
ed3f234b8e |
8 changed files with 400 additions and 29 deletions
1
changelog.d/63.md
Normal file
1
changelog.d/63.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
- Normalize Debian package payload modes independently of the builder's umask. (#63).
|
||||||
1
changelog.d/64.md
Normal file
1
changelog.d/64.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
- Report supplied tokens that remain active after logout and make unauthenticated status machine-detectable. (#64).
|
||||||
1
changelog.d/65.md
Normal file
1
changelog.d/65.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
- Continue batch imports after one repository cannot resolve its source token, while reporting that item as failed. (#65).
|
||||||
|
|
@ -67,9 +67,9 @@ EOF
|
||||||
# Native package (no Debian revision in the version), so plain changelog.gz.
|
# Native package (no Debian revision in the version), so plain changelog.gz.
|
||||||
gzip -9n -c "$STAGE/changelog" > "$DOC/changelog.gz"
|
gzip -9n -c "$STAGE/changelog" > "$DOC/changelog.gz"
|
||||||
|
|
||||||
# Normalize permissions regardless of the builder's umask: no group/other
|
# Normalize permissions regardless of the builder's umask: traversable
|
||||||
# write anywhere, executable entry point.
|
# directories, readable files, and execute bits retained only where intended.
|
||||||
chmod -R go-w "$PKG/usr"
|
chmod -R u+rwX,go=rX "$PKG/usr"
|
||||||
chmod 0755 "$LIB/src/cli.js"
|
chmod 0755 "$LIB/src/cli.js"
|
||||||
|
|
||||||
# --- control -----------------------------------------------------------------
|
# --- control -----------------------------------------------------------------
|
||||||
|
|
|
||||||
56
src/cli.js
56
src/cli.js
|
|
@ -295,6 +295,8 @@ auth
|
||||||
} else {
|
} else {
|
||||||
console.log(`Skipping remote revocation (no password provided). Token ${config.tokenId} stays active on ${config.url}; revoke it from the web UI under Settings > Applications.`);
|
console.log(`Skipping remote revocation (no password provided). Token ${config.tokenId} stays active on ${config.url}; revoke it from the web UI under Settings > Applications.`);
|
||||||
}
|
}
|
||||||
|
} else if (!config.tokenId && !options.localOnly) {
|
||||||
|
console.log(`Removing local credentials. Stoke did not create this token and cannot revoke it. The token is still valid on ${config.url}; revoke it from the web UI under Settings > Applications.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearConfig();
|
clearConfig();
|
||||||
|
|
@ -313,8 +315,12 @@ auth
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
if (!config || !config.token) {
|
if (!config || !config.token) {
|
||||||
console.log('Not authenticated.');
|
if (options.json) {
|
||||||
return;
|
console.log('{"authenticated": false}');
|
||||||
|
} else {
|
||||||
|
console.log('Not authenticated.');
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = ForgejoClient.fromConfig(config);
|
const client = ForgejoClient.fromConfig(config);
|
||||||
|
|
@ -649,30 +655,30 @@ repo
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = item.service || 'github';
|
|
||||||
const isPrivate = item.public ? false : Boolean(item.private);
|
|
||||||
const payload = {
|
|
||||||
clone_addr: from,
|
|
||||||
repo_name: name,
|
|
||||||
repo_owner: item.owner || item.repo_owner || config.login,
|
|
||||||
service,
|
|
||||||
description: item.description || undefined,
|
|
||||||
private: isPrivate,
|
|
||||||
issues: normalizeBool(item.issues, true),
|
|
||||||
labels: normalizeBool(item.labels, true),
|
|
||||||
milestones: normalizeBool(item.milestones, true),
|
|
||||||
pull_requests: normalizeBool(item.pull_requests, true),
|
|
||||||
releases: normalizeBool(item.releases, true),
|
|
||||||
wiki: normalizeBool(item.wiki, true),
|
|
||||||
lfs: normalizeBool(item.lfs, false),
|
|
||||||
auth_token: resolveSourceToken(item.github_token, service),
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(payload).forEach((key) => {
|
|
||||||
if (payload[key] === undefined) delete payload[key];
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const service = item.service || 'github';
|
||||||
|
const isPrivate = item.public ? false : Boolean(item.private);
|
||||||
|
const payload = {
|
||||||
|
clone_addr: from,
|
||||||
|
repo_name: name,
|
||||||
|
repo_owner: item.owner || item.repo_owner || config.login,
|
||||||
|
service,
|
||||||
|
description: item.description || undefined,
|
||||||
|
private: isPrivate,
|
||||||
|
issues: normalizeBool(item.issues, true),
|
||||||
|
labels: normalizeBool(item.labels, true),
|
||||||
|
milestones: normalizeBool(item.milestones, true),
|
||||||
|
pull_requests: normalizeBool(item.pull_requests, true),
|
||||||
|
releases: normalizeBool(item.releases, true),
|
||||||
|
wiki: normalizeBool(item.wiki, true),
|
||||||
|
lfs: normalizeBool(item.lfs, false),
|
||||||
|
auth_token: resolveSourceToken(item.github_token, service),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(payload).forEach((key) => {
|
||||||
|
if (payload[key] === undefined) delete payload[key];
|
||||||
|
});
|
||||||
|
|
||||||
const result = await client.migrateRepo(payload);
|
const result = await client.migrateRepo(payload);
|
||||||
console.log(`Imported: ${result.full_name} -> ${result.html_url}`);
|
console.log(`Imported: ${result.full_name} -> ${result.html_url}`);
|
||||||
results.push({ name, status: 'ok', url: result.html_url });
|
results.push({ name, status: 'ok', url: result.html_url });
|
||||||
|
|
|
||||||
65
test/build-deb.test.js
Normal file
65
test/build-deb.test.js
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const ROOT = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
function copyTree(source, destination) {
|
||||||
|
fs.cpSync(source, destination, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPackage(umask) {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-build-deb-test-'));
|
||||||
|
const bin = path.join(root, 'bin');
|
||||||
|
fs.mkdirSync(path.join(root, 'scripts'));
|
||||||
|
fs.mkdirSync(bin);
|
||||||
|
fs.copyFileSync(path.join(ROOT, 'scripts', 'build-deb.sh'), path.join(root, 'scripts', 'build-deb.sh'));
|
||||||
|
copyTree(path.join(ROOT, 'src'), path.join(root, 'src'));
|
||||||
|
fs.copyFileSync(path.join(ROOT, 'package.json'), path.join(root, 'package.json'));
|
||||||
|
fs.copyFileSync(path.join(ROOT, 'package-lock.json'), path.join(root, 'package-lock.json'));
|
||||||
|
|
||||||
|
const npm = path.join(bin, 'npm');
|
||||||
|
fs.writeFileSync(npm, '#!/usr/bin/env bash\nexit 0\n');
|
||||||
|
fs.chmodSync(npm, 0o755);
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
'bash',
|
||||||
|
['-c', 'umask "$1"; exec bash "$2"', 'build-deb-test', umask, path.join(root, 'scripts', 'build-deb.sh')],
|
||||||
|
{
|
||||||
|
encoding: 'utf8',
|
||||||
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH}` },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
|
||||||
|
const deb = path.join(root, 'dist', 'stoke_1.5.0_all.deb');
|
||||||
|
const listing = spawnSync('dpkg-deb', ['-c', deb], { encoding: 'utf8' });
|
||||||
|
assert.equal(listing.status, 0, listing.stderr);
|
||||||
|
|
||||||
|
const modes = new Map();
|
||||||
|
for (const line of listing.stdout.trim().split('\n')) {
|
||||||
|
const fields = line.trim().split(/\s+/);
|
||||||
|
const archivePath = fields.find((field) => field.startsWith('./usr/'));
|
||||||
|
if (archivePath && (fields[0].startsWith('d') || fields[0].startsWith('-'))) {
|
||||||
|
modes.set(archivePath, fields[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { root, modes };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Debian payload modes are identical under umask 077 and 022', (t) => {
|
||||||
|
const restrictive = buildPackage('077');
|
||||||
|
const standard = buildPackage('022');
|
||||||
|
t.after(() => {
|
||||||
|
fs.rmSync(restrictive.root, { recursive: true, force: true });
|
||||||
|
fs.rmSync(standard.root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(restrictive.modes, standard.modes);
|
||||||
|
for (const [archivePath, mode] of restrictive.modes) {
|
||||||
|
assert.equal(mode, archivePath.endsWith('/') ? 'drwxr-xr-x' : archivePath === './usr/lib/stoke/src/cli.js' ? '-rwxr-xr-x' : '-rw-r--r--', archivePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -40,10 +40,59 @@ test('global --config flag overrides the config location', () => {
|
||||||
// "Not authenticated" instead of silently using the default config.
|
// "Not authenticated" instead of silently using the default config.
|
||||||
const missing = path.join(os.tmpdir(), `stoke-missing-${process.pid}.json`);
|
const missing = path.join(os.tmpdir(), `stoke-missing-${process.pid}.json`);
|
||||||
const res = run(['--config', missing, 'auth', 'status']);
|
const res = run(['--config', missing, 'auth', 'status']);
|
||||||
assert.equal(res.status, 0);
|
assert.equal(res.status, 1);
|
||||||
assert.match(res.stdout, /Not authenticated/);
|
assert.match(res.stdout, /Not authenticated/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('auth status reports an absent session in text and JSON with a failing status', () => {
|
||||||
|
const missing = path.join(os.tmpdir(), `stoke-missing-${process.pid}-auth-status.json`);
|
||||||
|
|
||||||
|
const text = run(['auth', 'status'], { STOKE_CONFIG_FILE: missing });
|
||||||
|
assert.equal(text.status, 1);
|
||||||
|
assert.equal(text.stdout, 'Not authenticated.\n');
|
||||||
|
assert.equal(text.stderr, '');
|
||||||
|
|
||||||
|
const json = run(['auth', 'status', '--json'], { STOKE_CONFIG_FILE: missing });
|
||||||
|
assert.equal(json.status, 1);
|
||||||
|
assert.equal(json.stdout, '{"authenticated": false}\n');
|
||||||
|
assert.equal(json.stderr, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('auth logout identifies a supplied token that remains active without changing local-only output', () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-auth-logout-'));
|
||||||
|
const cfg = path.join(dir, 'config.json');
|
||||||
|
const config = {
|
||||||
|
url: 'https://forge.test',
|
||||||
|
login: 'bot',
|
||||||
|
username: 'bot',
|
||||||
|
token: 'token-that-must-not-be-printed',
|
||||||
|
tokenId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(cfg, JSON.stringify(config));
|
||||||
|
const logout = run(['auth', 'logout'], { STOKE_CONFIG_FILE: cfg });
|
||||||
|
assert.equal(logout.status, 0, logout.stderr);
|
||||||
|
assert.match(logout.stdout, /local credentials/i);
|
||||||
|
assert.match(logout.stdout, /did not create this token/i);
|
||||||
|
assert.match(logout.stdout, /cannot revoke it/i);
|
||||||
|
assert.match(logout.stdout, /still valid on https:\/\/forge\.test/i);
|
||||||
|
assert.match(logout.stdout, /Settings > Applications/);
|
||||||
|
assert.doesNotMatch(logout.stdout, /Revoked token/);
|
||||||
|
assert.doesNotMatch(logout.stdout, /Password for/);
|
||||||
|
assert.doesNotMatch(logout.stdout, /token-that-must-not-be-printed/);
|
||||||
|
assert.equal(fs.existsSync(cfg), false);
|
||||||
|
|
||||||
|
fs.writeFileSync(cfg, JSON.stringify(config));
|
||||||
|
const localOnly = run(['auth', 'logout', '--local-only'], { STOKE_CONFIG_FILE: cfg });
|
||||||
|
assert.equal(localOnly.status, 0, localOnly.stderr);
|
||||||
|
assert.equal(localOnly.stdout, 'Local credentials removed.\n');
|
||||||
|
assert.equal(fs.existsSync(cfg), false);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('invalid --limit is rejected before any network call', () => {
|
test('invalid --limit is rejected before any network call', () => {
|
||||||
const res = run(['repo', 'list', '-l', 'abc']);
|
const res = run(['repo', 'list', '-l', 'abc']);
|
||||||
assert.equal(res.status, 1);
|
assert.equal(res.status, 1);
|
||||||
|
|
|
||||||
248
test/import-batch.test.js
Normal file
248
test/import-batch.test.js
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { spawn } = require('node:child_process');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const http = require('node:http');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const CLI = path.join(__dirname, '..', 'src', 'cli.js');
|
||||||
|
|
||||||
|
function run(args, env = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const childEnv = { ...process.env, ...env };
|
||||||
|
childEnv.NODE_OPTIONS = [
|
||||||
|
childEnv.NODE_OPTIONS,
|
||||||
|
'--disable-warning=ExperimentalWarning',
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
const child = spawn(process.execPath, [CLI, ...args], {
|
||||||
|
env: childEnv,
|
||||||
|
});
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
child.stdout.setEncoding('utf8');
|
||||||
|
child.stderr.setEncoding('utf8');
|
||||||
|
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
||||||
|
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
||||||
|
child.on('error', reject);
|
||||||
|
child.on('close', (status) => resolve({ status, stdout, stderr }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startMigrationServer() {
|
||||||
|
const requests = [];
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
let body = '';
|
||||||
|
req.setEncoding('utf8');
|
||||||
|
req.on('data', (chunk) => { body += chunk; });
|
||||||
|
req.on('end', () => {
|
||||||
|
const payload = JSON.parse(body);
|
||||||
|
requests.push({ method: req.method, url: req.url, body: payload });
|
||||||
|
res.writeHead(201, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
full_name: `destination/${payload.repo_name}`,
|
||||||
|
html_url: `https://forge.test/destination/${payload.repo_name}`,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||||
|
return { server, requests };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('repo import-batch continues after one item has no source token', async () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-import-batch-'));
|
||||||
|
const configFile = path.join(dir, 'config.json');
|
||||||
|
const manifestFile = path.join(dir, 'manifest.json');
|
||||||
|
const emptyPath = path.join(dir, 'bin');
|
||||||
|
const forgeToken = 'forge-token-must-not-be-printed';
|
||||||
|
const { server, requests } = await startMigrationServer();
|
||||||
|
fs.mkdirSync(emptyPath);
|
||||||
|
fs.writeFileSync(configFile, JSON.stringify({
|
||||||
|
url: `http://127.0.0.1:${server.address().port}`,
|
||||||
|
login: 'destination',
|
||||||
|
token: forgeToken,
|
||||||
|
}));
|
||||||
|
fs.writeFileSync(manifestFile, JSON.stringify([
|
||||||
|
{ name: 'missing-token', from: 'https://github.com/source/first.git', service: 'github' },
|
||||||
|
{ name: 'imported-second', from: 'https://git.example/source/second.git', service: 'git' },
|
||||||
|
]));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await run(
|
||||||
|
['--config', configFile, 'repo', 'import-batch', '--file', manifestFile],
|
||||||
|
{ PATH: emptyPath, GITHUB_TOKEN: undefined },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.status, 1);
|
||||||
|
assert.match(result.stderr, /Failed to import missing-token: No GitHub token found\./);
|
||||||
|
assert.equal(result.stdout,
|
||||||
|
'Imported: destination/imported-second -> https://forge.test/destination/imported-second\n'
|
||||||
|
+ '\nBatch complete: 1/2 imported.\n');
|
||||||
|
assert.deepEqual(requests, [{
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/repos/migrate',
|
||||||
|
body: {
|
||||||
|
clone_addr: 'https://git.example/source/second.git',
|
||||||
|
repo_name: 'imported-second',
|
||||||
|
repo_owner: 'destination',
|
||||||
|
service: 'git',
|
||||||
|
private: false,
|
||||||
|
issues: true,
|
||||||
|
labels: true,
|
||||||
|
milestones: true,
|
||||||
|
pull_requests: true,
|
||||||
|
releases: true,
|
||||||
|
wiki: true,
|
||||||
|
lfs: false,
|
||||||
|
},
|
||||||
|
}]);
|
||||||
|
assert.doesNotMatch(result.stdout + result.stderr, new RegExp(forgeToken));
|
||||||
|
} finally {
|
||||||
|
await new Promise((resolve) => server.close(resolve));
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('repo import-batch preserves successful batch output', async () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-import-batch-success-'));
|
||||||
|
const configFile = path.join(dir, 'config.json');
|
||||||
|
const manifestFile = path.join(dir, 'manifest.json');
|
||||||
|
const { server, requests } = await startMigrationServer();
|
||||||
|
fs.writeFileSync(configFile, JSON.stringify({
|
||||||
|
url: `http://127.0.0.1:${server.address().port}`,
|
||||||
|
login: 'destination',
|
||||||
|
token: 'forge-token-must-not-be-printed',
|
||||||
|
}));
|
||||||
|
fs.writeFileSync(manifestFile, JSON.stringify([
|
||||||
|
{ name: 'first', from: 'https://git.example/source/first.git', service: 'git' },
|
||||||
|
{ name: 'second', from: 'https://git.example/source/second.git', service: 'git' },
|
||||||
|
]));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await run(['--config', configFile, 'repo', 'import-batch', '--file', manifestFile]);
|
||||||
|
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(result.stderr, '');
|
||||||
|
assert.equal(result.stdout,
|
||||||
|
'Imported: destination/first -> https://forge.test/destination/first\n'
|
||||||
|
+ 'Imported: destination/second -> https://forge.test/destination/second\n'
|
||||||
|
+ '\nBatch complete: 2/2 imported.\n');
|
||||||
|
assert.deepEqual(requests.map(({ body }) => body.repo_name), ['first', 'second']);
|
||||||
|
} finally {
|
||||||
|
await new Promise((resolve) => server.close(resolve));
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('repo import-batch sends an explicit GitHub token without printing it', async () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-import-batch-token-'));
|
||||||
|
const configFile = path.join(dir, 'config.json');
|
||||||
|
const manifestFile = path.join(dir, 'manifest.json');
|
||||||
|
const emptyPath = path.join(dir, 'bin');
|
||||||
|
const sourceToken = 'github-token-must-not-be-printed';
|
||||||
|
const { server, requests } = await startMigrationServer();
|
||||||
|
fs.mkdirSync(emptyPath);
|
||||||
|
fs.writeFileSync(configFile, JSON.stringify({
|
||||||
|
url: `http://127.0.0.1:${server.address().port}`,
|
||||||
|
login: 'destination',
|
||||||
|
token: 'forge-token-must-not-be-printed',
|
||||||
|
}));
|
||||||
|
fs.writeFileSync(manifestFile, JSON.stringify([{
|
||||||
|
name: 'from-github',
|
||||||
|
from: 'https://github.com/source/repository.git',
|
||||||
|
service: 'github',
|
||||||
|
github_token: sourceToken,
|
||||||
|
}]));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await run(
|
||||||
|
['--config', configFile, 'repo', 'import-batch', '--file', manifestFile],
|
||||||
|
{ PATH: emptyPath, GITHUB_TOKEN: undefined },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(result.stderr, '');
|
||||||
|
assert.equal(result.stdout,
|
||||||
|
'Imported: destination/from-github -> https://forge.test/destination/from-github\n'
|
||||||
|
+ '\nBatch complete: 1/1 imported.\n');
|
||||||
|
assert.deepEqual(requests, [{
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/repos/migrate',
|
||||||
|
body: {
|
||||||
|
clone_addr: 'https://github.com/source/repository.git',
|
||||||
|
repo_name: 'from-github',
|
||||||
|
repo_owner: 'destination',
|
||||||
|
service: 'github',
|
||||||
|
private: false,
|
||||||
|
issues: true,
|
||||||
|
labels: true,
|
||||||
|
milestones: true,
|
||||||
|
pull_requests: true,
|
||||||
|
releases: true,
|
||||||
|
wiki: true,
|
||||||
|
lfs: false,
|
||||||
|
auth_token: sourceToken,
|
||||||
|
},
|
||||||
|
}]);
|
||||||
|
assert.doesNotMatch(result.stdout + result.stderr, new RegExp(sourceToken));
|
||||||
|
} finally {
|
||||||
|
await new Promise((resolve) => server.close(resolve));
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('repo import-batch keeps file and JSON errors at batch level', async () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-import-batch-invalid-'));
|
||||||
|
const configFile = path.join(dir, 'config.json');
|
||||||
|
const missingFile = path.join(dir, 'missing.json');
|
||||||
|
const malformedFile = path.join(dir, 'malformed.json');
|
||||||
|
fs.writeFileSync(configFile, JSON.stringify({
|
||||||
|
url: 'https://forge.test',
|
||||||
|
login: 'destination',
|
||||||
|
token: 'forge-token-must-not-be-printed',
|
||||||
|
}));
|
||||||
|
fs.writeFileSync(malformedFile, '{not json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const missing = await run(['--config', configFile, 'repo', 'import-batch', '--file', missingFile]);
|
||||||
|
const malformed = await run(['--config', configFile, 'repo', 'import-batch', '--file', malformedFile]);
|
||||||
|
|
||||||
|
assert.equal(missing.status, 1);
|
||||||
|
assert.match(missing.stderr, /^Batch import failed: ENOENT:/);
|
||||||
|
assert.equal(missing.stdout, '');
|
||||||
|
assert.equal(malformed.status, 1);
|
||||||
|
assert.match(malformed.stderr, /^Batch import failed: /);
|
||||||
|
assert.match(malformed.stderr, /JSON/);
|
||||||
|
assert.equal(malformed.stdout, '');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('repo import-batch excludes skipped invalid entries from the summary', async () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-import-batch-skip-'));
|
||||||
|
const configFile = path.join(dir, 'config.json');
|
||||||
|
const manifestFile = path.join(dir, 'manifest.json');
|
||||||
|
const { server, requests } = await startMigrationServer();
|
||||||
|
fs.writeFileSync(configFile, JSON.stringify({
|
||||||
|
url: `http://127.0.0.1:${server.address().port}`,
|
||||||
|
login: 'destination',
|
||||||
|
token: 'forge-token-must-not-be-printed',
|
||||||
|
}));
|
||||||
|
fs.writeFileSync(manifestFile, JSON.stringify([
|
||||||
|
{ name: 'missing-source' },
|
||||||
|
{ name: 'valid', from: 'https://git.example/source/valid.git', service: 'git' },
|
||||||
|
]));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await run(['--config', configFile, 'repo', 'import-batch', '--file', manifestFile]);
|
||||||
|
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(result.stderr, 'Skipping invalid manifest entry: {"name":"missing-source"}\n');
|
||||||
|
assert.match(result.stdout, /Batch complete: 1\/1 imported\./);
|
||||||
|
assert.deepEqual(requests.map(({ body }) => body.repo_name), ['valid']);
|
||||||
|
} finally {
|
||||||
|
await new Promise((resolve) => server.close(resolve));
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue