Skip to content
Snippets Groups Projects
Commit d50ee1af authored by Joshua Leivers's avatar Joshua Leivers
Browse files

feat: add checks for paths and archive writeability

Plugin previously allowed absolute paths and did not
treat configured paths as being relative to the
contextual `cwd`.
Also now generates archive output path if it doesn't
already exist.
parent 8f9abf02
No related branches found
No related tags found
1 merge request!1Prepare for OSS release
......@@ -29,12 +29,12 @@ plugins:
### `inputs`
The paths to add to the archive.
The relative paths to add to the archive.
These can be directories and/or filepaths.
### `archive`
The path where the archive should be created.
The relative path where the archive should be created.
Defaults to `archive.tar.gz`.
import {lstatSync, createWriteStream} from 'node:fs';
import {access} from 'node:fs/promises';
import {access, writeFile, rm, mkdir} from 'node:fs/promises';
import {dirname, isAbsolute, join} from 'node:path';
import SemanticReleaseError from '@semantic-release/error';
import archiver from 'archiver';
import dbg from 'debug';
......@@ -33,36 +34,74 @@ function extension(path) {
}
export async function verifyConditions(pluginConfig, context) {
const {logger} = context;
const {inputs, archive = DEFAULT_ARCHIVE_PATH} = pluginConfig;
const {logger, cwd} = context;
const {inputs: relativeInputs, archive: relativeArchive = DEFAULT_ARCHIVE_PATH} = pluginConfig;
debug('Validating input paths');
// Make sure we have the correct shape for the paths
if (!Array.isArray(inputs)) {
if (!Array.isArray(relativeInputs)) {
throw new SemanticReleaseError(
'`inputs` must be a list',
'EARCHIVECFG',
verbatim(inputs),
verbatim(relativeInputs),
);
}
if (inputs.length === 0) {
if (relativeInputs.length === 0) {
throw new SemanticReleaseError(
'`inputs` must be non-empty',
'EARCHIVECFG',
verbatim(inputs),
verbatim(relativeInputs),
);
}
if (typeof archive !== 'string') {
// Make sure that the input paths are relative
const absolute = relativeInputs.filter((_, i) => isAbsolute(relativeInputs[i]));
if (absolute.length > 0) {
throw new SemanticReleaseError(
`Input paths are absolute: \`${absolute.join(', ')}\``,
'EARCHIVECFG',
verbatim(absolute),
);
}
// Get the absolute input paths based on the working directory
const inputs = relativeInputs.map(path => join(cwd, path));
// Make sure that the input paths exist
const exist = await Promise.allSettled(inputs.map(p => access(p)));
const missing = inputs.filter((_, i) => exist[i].status === 'rejected');
if (missing.length > 0) {
throw new SemanticReleaseError(
`Input paths do not exist: \`${missing.join(', ')}\``,
'EARCHIVECFG',
verbatim(missing),
);
}
logger.success('Input paths are valid');
debug('Validating output path');
if (typeof relativeArchive !== 'string') {
throw new SemanticReleaseError(
'`archive` is not a string',
'EARCHIVECFG',
verbatim(archive),
verbatim(relativeArchive),
);
}
// Make sure that the archive output path isn't an absolute path
if (isAbsolute(relativeArchive)) {
throw new SemanticReleaseError(
'`archive` is an absolute path',
'EARCHIVECFG',
verbatim(relativeArchive),
);
}
const archive = join(cwd, relativeArchive);
if (!Object.keys(COMPRESSION).includes(extension(archive))) {
throw new SemanticReleaseError(
'`archive` has unsupported extension',
......@@ -71,24 +110,39 @@ export async function verifyConditions(pluginConfig, context) {
);
}
// Make sure that the input paths exist
const exist = await Promise.allSettled(inputs.map(p => access(p)));
const missing = inputs.filter((_, i) => exist[i].status === 'rejected');
if (missing.length > 0) {
// Create the output directory if it doesn't already exist
const directory = dirname(archive);
try {
await mkdir(directory, {recursive: true});
} catch (error) {
throw new SemanticReleaseError(
`Input paths do not exist: \`${missing.join(', ')}\``,
`Failed to create output directory \`${directory}\``,
'EARCHIVECFG',
verbatim(missing),
verbatim(error),
);
}
logger.success('Input paths are valid');
// Make sure we have write permissions to the archive path
try {
await writeFile(archive, '');
await rm(archive);
} catch (error) {
throw new SemanticReleaseError(
`Cannot write to \`archive\` path: ${archive}`,
'EARCHIVECFG',
verbatim(error),
);
}
logger.success('Output path is valid');
}
export async function prepare(pluginConfig, context) {
const {logger} = context;
const {logger, cwd} = context;
const {nextRelease: {version}} = context;
const {inputs, archive = DEFAULT_ARCHIVE_PATH} = pluginConfig;
const {inputs: relativeInputs, archive: relativeArchive = DEFAULT_ARCHIVE_PATH} = pluginConfig;
const archive = join(cwd, relativeArchive);
const inputs = relativeInputs.map(path => join(cwd, path));
await new Promise((resolve, reject) => {
const archiveStream = archiver(...COMPRESSION[extension(archive)]);
......
main.js
\ No newline at end of file
main.js
data/output/
\ No newline at end of file
File moved
File moved
import {env} from 'node:process';
import {cp, mkdir} from 'node:fs/promises';
import {dirname, join} from 'node:path';
import test from 'ava';
import {stub} from 'sinon';
import clearModule from 'clear-module';
......@@ -10,7 +12,8 @@ test.beforeEach(async t => {
t.context.m = await import('../plugin.mjs');
t.context.cfg = {
inputs: ['test/data/file.txt', 'test/data/folder'],
inputs: ['test/fixture/file.txt', 'test/fixture/folder'],
archive: 'nested/archive.tar.gz',
};
const environ = Object.fromEntries(Object.entries(env));
......@@ -28,6 +31,9 @@ test.beforeEach(async t => {
warn: t.context.log,
},
};
// Copy the test fixtures to the test's temporary directory
await cp('test/fixture/', join(t.context.ctx.cwd, 'test/fixture/'), {recursive: true});
});
const failure = test.macro({
......@@ -82,7 +88,7 @@ test(failure, 'EARCHIVECFG', /`inputs` must be non-empty/, async t => {
});
test(failure, 'EARCHIVECFG', /Input paths do not exist: `.+`/, async t => {
t.context.cfg.inputs = ['/this/does/not/exist'];
t.context.cfg.inputs = ['this/does/not/exist'];
});
test(failure, 'EARCHIVECFG', /`archive` is not a string/, async t => {
......@@ -93,4 +99,28 @@ test(failure, 'EARCHIVECFG', /`archive` has unsupported extension/, async t => {
t.context.cfg.archive = 'archive.undefined';
});
test(failure, 'EARCHIVECFG', /Cannot write to `archive` path/, async t => {
const {cwd} = t.context.ctx;
const {archive} = t.context.cfg;
const parent = join(cwd, dirname(archive));
// Use output directory with no write permissions
await mkdir(parent, {mode: 0o555}); // 0o555 is r-xr-xr-x
});
test(failure, 'EARCHIVECFG', /`archive` is an absolute path/, async t => {
t.context.cfg.archive = '/home/test/archive.tar.gz';
});
test(failure, 'EARCHIVECFG', /Input paths are absolute: `.+`/, async t => {
t.context.cfg.inputs = ['/test/fixture/file.txt', 'test/fixture/folder'];
});
test(failure, 'EARCHIVECFG', /Failed to create output directory `.+`/, async t => {
// Set output directory to be within an unwriteable parent directory
t.context.cfg.archive = 'nested/parent/archive.tar.gz';
const nested = join(t.context.ctx.cwd, 'nested/');
await mkdir(nested, {mode: 0o555}); // 0o555 is r-xr-xr-x
});
test(success);
  • Congregate Migrate @congregate_migrate

    mentioned in commit 4b72ebdb

    By GITLAB_TOKEN on 2023-10-16T14:38:44

    · Imported

    mentioned in commit 4b72ebdb

    By GITLAB_TOKEN on 2023-10-16T14:38:44

    Toggle commit list
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment