diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ccdbf98..04e37e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: check-repo: ${{ github.event_name == 'push' }} publish: # publish dependency-free release with only self-contained dist directory - if: github.repository == 'katyo/publish-crates' && github.event_name == 'push' && github.ref_name == 'main' + if: github.event_name == 'push' && github.ref_name == 'main' runs-on: ubuntu-latest needs: - build diff --git a/README.md b/README.md index 1a2cd43..f88eae2 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ # Publish Rust crates using GitHub Actions +The action is using [`cargo metadata`](https://doc.rust-lang.org/cargo/commands/cargo-metadata.html) with format version +`1` to collect the information about crates and workspace. + ## Features - Reads manifests to get info about crates and dependencies @@ -13,6 +16,8 @@ - Publishes updated crates in right order according to dependencies - Awaits when published crate will be available in registry before publishing crates which depends from it - Works fine in workspaces without cyclic dependencies +- Support `{ workspace = true }` syntax in the `Cargo.toml`. [This](https://rust-lang.github.io/rfcs/2906-cargo-workspace-deduplicate.html) + feature was stabilized in Rust 1.64. ## Unimplemented features diff --git a/__tests__/Cargo.toml b/__tests__/Cargo.toml index b231b67..02fb5ff 100644 --- a/__tests__/Cargo.toml +++ b/__tests__/Cargo.toml @@ -1,19 +1,31 @@ [workspace] members = [ "pkg-sys", + "pkg-skip", "pkg-build", "pkg-lib", "pkg-bin", 'pkg-dev', ] +[workspace.package] +version = "0.1.0" + [package] name = "pkg-all" version = "0.1.0" [dependencies] pkg-lib = { version = "0.1.0", path = "./pkg-lib" } +subcrate-d = { workspace = true, path = "../workspace/subcrate_d" } +subcrate-e = { workspace = true, path = "../workspace/subcrate_e" } +subcrate-f = { workspace = true, path = "../workspace/subcrate_f" } [dependencies.pkg-bin] version = "0.1.0" path = "./pkg-bin" + +[workspace.dependencies] +subcrate-d = { version = "0.1.0", path = "./workspace/subcrate_d" } +subcrate-e = { version = "0.1.0", path = "./workspace/subcrate_e" } +subcrate-f = { version = "0.1.0", path = "./workspace/subcrate_f" } diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index bbec1da..1f64a87 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -3,22 +3,22 @@ import {findPackages, checkPackages, sortPackages} from '../src/package' import {githubHandle, lastCommitDate} from '../src/github' import {semver} from '../src/utils' import {join} from 'path' -import {exec} from '@actions/exec' const pkg_dir = __dirname test('find packages', async () => { const packages = await findPackages(pkg_dir) - expect(Object.keys(packages).length).toBe(6) + expect(Object.keys(packages).length).toBe(9) const pkg_all = packages['pkg-all'] const pkg_sys = packages['pkg-sys'] const pkg_lib = packages['pkg-lib'] const pkg_bin = packages['pkg-bin'] + const subcrate_e = packages['subcrate-e'] expect(pkg_all.path).toBe(pkg_dir) expect(pkg_all.version).toBe('0.1.0') - expect(Object.keys(pkg_all.dependencies).length).toBe(2) + expect(Object.keys(pkg_all.dependencies).length).toBe(5) expect(pkg_sys.path).toBe(join(pkg_dir, 'pkg-sys')) expect(pkg_sys.version).toBe('0.1.0') @@ -31,6 +31,10 @@ test('find packages', async () => { expect(pkg_bin.path).toBe(join(pkg_dir, 'pkg-bin')) expect(pkg_bin.version).toBe('0.1.0') expect(Object.keys(pkg_bin.dependencies).length).toBe(3) + + expect(subcrate_e.path).toBe(join(pkg_dir, 'workspace/subcrate_e')) + expect(subcrate_e.version).toBe('0.1.0') + expect(Object.keys(subcrate_e.dependencies).length).toBe(1) }) test('check packages', async () => { @@ -46,6 +50,9 @@ test('sort packages', async () => { 'pkg-sys', 'pkg-lib', 'pkg-build', + 'subcrate-d', + 'subcrate-f', + 'subcrate-e', 'pkg-dev', 'pkg-bin', 'pkg-all' diff --git a/__tests__/pkg-lib/Cargo.toml b/__tests__/pkg-lib/Cargo.toml index faf390a..ce6dc0d 100644 --- a/__tests__/pkg-lib/Cargo.toml +++ b/__tests__/pkg-lib/Cargo.toml @@ -9,6 +9,9 @@ lazy_static = "1.0" version = "0.1.0" path = "../pkg-sys" +[dev-dependencies] +lazy_static = "1.0" + # test that local cyclic dev-deps are allowed using paths [dev-dependencies.pkg-dev] path = "../pkg-dev" \ No newline at end of file diff --git a/__tests__/pkg-skip/Cargo.toml b/__tests__/pkg-skip/Cargo.toml new file mode 100644 index 0000000..bbaef60 --- /dev/null +++ b/__tests__/pkg-skip/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pkg-skip" +version = "0.1.0" +publish = false + +[dependencies] +pkg-lib = { version = "0.1.0", path = "../pkg-lib" } +clap = "^2" + +[dev-dependencies.pkg-dev] +version = "0.1.0" +path = "../pkg-dev" \ No newline at end of file diff --git a/__tests__/pkg-skip/src/main.rs b/__tests__/pkg-skip/src/main.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/__tests__/pkg-skip/src/main.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/__tests__/workspace/subcrate_d/Cargo.toml b/__tests__/workspace/subcrate_d/Cargo.toml new file mode 100644 index 0000000..40c16e6 --- /dev/null +++ b/__tests__/workspace/subcrate_d/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "subcrate-d" +version = { workspace = true } + +[dev-dependencies.subcrate-f] +path = "../subcrate_f" \ No newline at end of file diff --git a/__tests__/workspace/subcrate_d/src/lib.rs b/__tests__/workspace/subcrate_d/src/lib.rs new file mode 100644 index 0000000..697efbb --- /dev/null +++ b/__tests__/workspace/subcrate_d/src/lib.rs @@ -0,0 +1,10 @@ +pub const TEST_VALUE: u8 = 5u8; + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} diff --git a/__tests__/workspace/subcrate_e/Cargo.toml b/__tests__/workspace/subcrate_e/Cargo.toml new file mode 100644 index 0000000..140391f --- /dev/null +++ b/__tests__/workspace/subcrate_e/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "subcrate-e" +version = { workspace = true } + +[dependencies.subcrate-d] +workspace = true +path = "../subcrate_d" + +[dev-dependencies.subcrate-f] +path = "../subcrate_f" \ No newline at end of file diff --git a/__tests__/workspace/subcrate_e/src/lib.rs b/__tests__/workspace/subcrate_e/src/lib.rs new file mode 100644 index 0000000..a83ef8d --- /dev/null +++ b/__tests__/workspace/subcrate_e/src/lib.rs @@ -0,0 +1,11 @@ +pub use fuel_dummy_test_subcrate_d::TEST_VALUE as OTHER_TEST_VALUE; + +#[cfg(test)] +mod tests { + pub use fuel_dummy_test_subcrate_f::TEST_VALUE; + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} diff --git a/__tests__/workspace/subcrate_f/Cargo.toml b/__tests__/workspace/subcrate_f/Cargo.toml new file mode 100644 index 0000000..f24cbc6 --- /dev/null +++ b/__tests__/workspace/subcrate_f/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "subcrate-f" +version = { workspace = true } + +[dependencies] +lazy_static = "1.0" + +[dev-dependencies] +lazy_static = "1.0" \ No newline at end of file diff --git a/__tests__/workspace/subcrate_f/src/lib.rs b/__tests__/workspace/subcrate_f/src/lib.rs new file mode 100644 index 0000000..697efbb --- /dev/null +++ b/__tests__/workspace/subcrate_f/src/lib.rs @@ -0,0 +1,10 @@ +pub const TEST_VALUE: u8 = 5u8; + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} diff --git a/src/package.ts b/src/package.ts index 0623323..ffbfccc 100644 --- a/src/package.ts +++ b/src/package.ts @@ -1,56 +1,31 @@ -import {dirname, join, normalize, relative, resolve} from 'path' -import {parse} from '@iarna/toml' -import {create as glob} from '@actions/glob' +import {dirname, join, normalize, relative} from 'path' +import {exec} from '@actions/exec' import {GitHubHandle, lastCommitDate} from './github' -import {readFile, semver, stat} from './utils' +import {semver} from './utils' import {getCrateVersions} from './crates' interface RawDependencies { - [name: string]: - | string - | { - package?: string - version?: string - path?: string - } + name: string + kind: string | null + req: string + path?: string +} + +interface Metadata { + packages: [RawManifest] } interface RawManifest { - workspace?: { - members?: string[] - } - package?: { - name?: string - version?: string - publish?: boolean - } - dependencies?: RawDependencies - 'dev-dependencies'?: RawDependencies - 'build-dependencies'?: RawDependencies + name?: string + manifest_path: string + version?: string + publish?: [string] + dependencies: [RawDependencies] } const manifest_filename = 'Cargo.toml' -async function readManifest(path: string): Promise { - try { - await stat(path) - } catch (error) { - throw new Error(`Manifest file '${path}' not found (${error})`) - } - let raw - try { - raw = await readFile(path, 'utf-8') - } catch (error) { - throw new Error(`Error when reading manifest file '${path}' (${error})`) - } - try { - return parse(raw) - } catch (error) { - throw new Error(`Error when parsing manifest file '${path}' (${error})`) - } -} - export interface Package { path: string version: string @@ -59,7 +34,7 @@ export interface Package { } export interface Dependency { - version: string + req: string path?: string } @@ -79,95 +54,106 @@ export async function findPackages( ? base_path : join(base_path, manifest_filename) const path = dirname(manifest_path) + const command = `cargo` + const args = [ + `metadata`, + `--no-deps`, + `--format-version`, + `1`, + `--manifest-path`, + `${manifest_path}` + ] - const manifest = await readManifest(manifest_path) + let output = '' + let exec_error = '' - if (typeof manifest.package === 'object') { - const {package: package_info} = manifest + await exec(command, args, { + listeners: { + stdout: (data: Buffer) => { + output += data.toString('utf8') + }, + stderr: (data: Buffer) => { + exec_error += data.toString('utf8') + } + } + }) + + if (exec_error.length > 0) { + throw new Error( + `During "cargo metadata" execution got an error: '${exec_error}'` + ) + } + + let metadata: Metadata + + try { + metadata = JSON.parse(output) + } catch (error) { + throw new Error(`Error when parsing manifest file '${path}' (${error})`) + } + + for (const package_info of metadata.packages) { if (typeof package_info.name !== 'string') { throw new Error(`Missing package name at '${path}'`) } if (typeof package_info.version !== 'string') { throw new Error(`Missing package version at '${path}'`) } - if (package_info.publish !== false) { + // List of registries to which this package may be published. + // Publishing is unrestricted if null, and forbidden if an empty array. + if (!package_info.publish || package_info.publish.length > 0) { const dependencies: Dependencies = {} - for (const [dependency_type, manifest_dependencies] of [ - ['normal', manifest.dependencies], - ['dev', manifest['dev-dependencies']], - ['build', manifest['build-dependencies']] - ]) { - if (typeof manifest_dependencies === 'object') { - for (const name in manifest_dependencies) { - const dependency = manifest_dependencies[name] - if (typeof dependency === 'string') { - dependencies[name] = {version: dependency} - } else if (typeof dependency == 'object') { - if ( - !dependency.version && - // normal and build deps require a version - dependency_type !== 'dev' - ) { - throw new Error( - `Missing dependency '${name}' version field` - ) - } else if ( - // throw an error if there is no path or version on dev-dependencies - dependency_type === 'dev' && - !dependency.version && - !dependency.path - ) { - throw new Error( - `Missing dependency '${name}' version field` - ) - } else if (dependency.version) { - // only include package in dependency graph if version is specified - const package_name = - typeof dependency.package === 'string' - ? dependency.package - : name - dependencies[package_name] = { - version: dependency.version, - path: dependency.path - } - } - } + for (const dependency of package_info.dependencies) { + const no_version = dependency.req === '*' + const kind = dependency.kind + const name = dependency.name + + if ( + no_version && + // normal and build deps require a version + kind !== 'dev' + ) { + throw new Error( + `Missing dependency '${name}' version field` + ) + } else if ( + // throw an error if there is no path or version on dev-dependencies + kind === 'dev' && + no_version && + !dependency.path + ) { + throw new Error( + `Missing dependency '${name}' version field` + ) + } else if (!no_version) { + // only include package in dependency graph if version is specified + let dependency_path + + if (dependency.path) { + dependency_path = relative( + dirname(package_info.manifest_path), + dependency.path + ) + } + + dependencies[name] = { + req: dependency.req, + path: dependency_path } } } packages[package_info.name] = { - path, + path: dirname( + join(path, relative(path, package_info.manifest_path)) + ), version: package_info.version, dependencies } } } - if (typeof manifest.workspace == 'object') { - const tasks: Promise[] = [] - const {workspace} = manifest - if (Array.isArray(workspace.members)) { - const globber = await glob( - workspace.members - .map(member => join(path, member, manifest_filename)) - .join('\n') - ) - const members_paths = await globber.glob() - const parent_path = resolve(path) - for (const member_path of members_paths) { - tasks.push( - findPackages( - join(path, relative(parent_path, member_path)), - packages - ) - ) - } - } - await Promise.all(tasks) - } - return packages } @@ -251,10 +237,10 @@ export async function checkPackages( message: `Package '${package_name}' depends from internal '${dependency_name}' with path '${dependency_path}' but actual path is '${dependency_package.path}'` }) } - if (!semver(dependency_package.version, dependency.version)) { + if (!semver(dependency_package.version, dependency.req)) { errors.push({ kind: 'mismatch-intern-dep-version', - message: `Package '${package_name}' depends from internal '${dependency_name}' with version '${dependency.version}' but actual version is '${dependency_package.version}'` + message: `Package '${package_name}' depends from internal '${dependency_name}' with version '${dependency.req}' but actual version is '${dependency_package.version}'` }) } } else { @@ -270,7 +256,7 @@ export async function checkPackages( } else { if ( !versions.some(({version}) => - semver(version, dependency.version) + semver(version, dependency.req) ) ) { const versions_string = versions @@ -278,7 +264,7 @@ export async function checkPackages( .join(', ') errors.push({ kind: 'mismatch-extern-dep-version', - message: `Package '${package_name}' depends from external '${dependency_name}' with version '${dependency.version}' which does not satisfies any of '${versions_string}'` + message: `Package '${package_name}' depends from external '${dependency_name}' with version '${dependency.req}' which does not satisfies any of '${versions_string}'` }) } }