From b89a1868f8396c796f91c47654573c6536bc5c1e Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sat, 9 Jul 2022 17:40:53 -0500 Subject: [PATCH] Added JSON-based changes to auto-generate the changelog. Solves merge conflicts between changelogs, by using sorting and type-based merging for the changelog from a series of JSON files. This allows conflict-free generation of a changelog using JSON-based changes, stored in `.changes`. Each entry can be object or array-based: the array must contain objects, where each object is a different change. The entries are added to the changelog, in sorted order, and the validity of the changelog is verified. To ensure fidelity of the changelog changes, the `no changelog` flag or at least 1 change (either removed, or added/dified and almovidated) for a JSON file under `.changes` must occur. A `pre-release-hook` has been added so all these changes are automerged into the CHANGELOG prior to release. --- .changes/918.json | 5 + .changes/README.md | 46 ++ .changes/template/940.json | 22 + .changes/template/978.json | 5 + .changes/template/979-981.json | 5 + .changes/template/CHANGELOG.md | 375 ++++++++++++++ .changes/template/issue440.json | 5 + .github/workflows/changelog.yml | 37 +- .gitignore | 3 +- CHANGELOG.md | 2 +- Cargo.lock | 52 +- Cargo.toml | 17 +- xtask/Cargo.toml | 1 + xtask/src/changelog.rs | 832 ++++++++++++++++++++++++++++++++ xtask/src/main.rs | 15 + 15 files changed, 1396 insertions(+), 26 deletions(-) create mode 100644 .changes/918.json create mode 100644 .changes/README.md create mode 100644 .changes/template/940.json create mode 100644 .changes/template/978.json create mode 100644 .changes/template/979-981.json create mode 100644 .changes/template/CHANGELOG.md create mode 100644 .changes/template/issue440.json create mode 100644 xtask/src/changelog.rs diff --git a/.changes/918.json b/.changes/918.json new file mode 100644 index 000000000..4316604e1 --- /dev/null +++ b/.changes/918.json @@ -0,0 +1,5 @@ +{ + "description": "use JSON-based files to autogenerate CHANGELOG.md", + "issues": [662], + "type": "internal" +} diff --git a/.changes/README.md b/.changes/README.md new file mode 100644 index 000000000..9f619d02d --- /dev/null +++ b/.changes/README.md @@ -0,0 +1,46 @@ +# Changes + +This stores changes to automatically generate the changelog, to avoid merge conflicts. Files should be in a JSON format, with the following format: + +```json +{ + "description": "single-line description to add to the CHANGELOG.", + "issues": [894], + "type": "added", + "breaking": false +} +``` + +Valid types are: +- added (Added) +- changed (Changed) +- fixed (Fixed) +- removed (Removed) +- internal (Internal) + +`breaking` is optional and defaults to false. if `breaking` is present for any active changes, a `BREAKING:` notice will be added at the start of the entry. `issues` is also optional, and is currently unused, and is an array of issues fixed by the PR, and defaults to an empty array. + +The file numbers should be `${pr}.json`. The `pr` is optional, and if not, an issue number should be used, in the `_${issue}.json` format. We also support multiple PRs per entry, using the `${pr1}-${pr2}-(...).json` format. + +If multiple changes are made in a single PR, you can also pass an array of entries: + +```json +[ + { + "description": "this is one added entry.", + "issues": [630], + "type": "added" + }, + { + "description": "this is another added entry.", + "issues": [642], + "type": "added" + }, + { + "description": "this is a fixed entry that has no attached issue.", + "type": "fixed" + } +] +``` + +See [template](/.changes/template) for sample object and array-based changes. diff --git a/.changes/template/940.json b/.changes/template/940.json new file mode 100644 index 000000000..d47669e8e --- /dev/null +++ b/.changes/template/940.json @@ -0,0 +1,22 @@ +[ + { + "description": "this is one added entry.", + "issues": [630], + "type": "added" + }, + { + "description": "this is another added entry.", + "issues": [642], + "type": "added" + }, + { + "description": "this is a fixed entry that has no attached issue.", + "type": "fixed" + }, + { + "description": "this is a breaking change.", + "issues": [679], + "breaking": true, + "type": "changed" + } +] diff --git a/.changes/template/978.json b/.changes/template/978.json new file mode 100644 index 000000000..55c574ba2 --- /dev/null +++ b/.changes/template/978.json @@ -0,0 +1,5 @@ +{ + "description": "sample description for a PR adding one CHANGELOG entry.", + "issues": [437], + "type": "fixed" +} diff --git a/.changes/template/979-981.json b/.changes/template/979-981.json new file mode 100644 index 000000000..de7b0a90f --- /dev/null +++ b/.changes/template/979-981.json @@ -0,0 +1,5 @@ +{ + "description": "this has 2 PRs associated.", + "issues": [441], + "type": "added" +} diff --git a/.changes/template/CHANGELOG.md b/.changes/template/CHANGELOG.md new file mode 100644 index 000000000..5574fb02f --- /dev/null +++ b/.changes/template/CHANGELOG.md @@ -0,0 +1,375 @@ +# Change Log + +This is a template changelog. This represents an older state of this repository, used to test parsing/formatting. + + + +## [Unreleased] - ReleaseDate + +### Added + +- #905 - added `qemu-runner` for musl images, allowing use of native or emulated runners. +- #905 - added qemu emulation to `i586-unknown-linux-gnu`, `i686-unknown-linux-musl`, and `i586-unknown-linux-gnu`, so they can run on an `x86` CPU, rather than an `x86_64` CPU. +- #900 - add the option to skip copying build artifacts back to host when using remote cross via `CROSS_REMOTE_SKIP_BUILD_ARTIFACTS`. +- #891 - support custom user namespace overrides by setting the `CROSS_CONTAINER_USER_NAMESPACE` environment variable. +- #890 - support rootless docker via the `CROSS_ROOTLESS_CONTAINER_ENGINE` environment variable. +- #878 - added an image `ghcr.io/cross-rs/cross` containing cross. + +### Changed + +- #869 - ensure cargo configuration environment variable flags are passed to the docker container. +- #859 - added color diagnostic output and error messages. + +### Fixed + +- #905 - fixed running dynamically-linked libraries for all musl targets except `x86_64-unknown-linux-musl`. +- #904 - ensure `cargo metadata` works by using the same channel. +- #904 - fixed the path for workspace volumes and passthrough volumes with docker-in-docker. +- #898 - fix the path to the mount root with docker-in-docker if mounting volumes. +- #897 - ensure `target.$(...)` config options override `build` ones when parsing strings and vecs. +- #895 - convert filenames in docker tags to ASCII lowercase and ignore invalid characters +- #885 - handle symlinks when using remote docker. +- #868 - ignore the `CARGO` environment variable. +- #867 - fixed parsing of `build.env.passthrough` config values. + +## [v0.2.2] - 2022-06-24 + +### Added + +- #803 - added `CROSS_CUSTOM_TOOLCHAIN` to disable automatic installation of components for use with tools like `cargo-bisect-rustc` +- #795 - added images for additional toolchains maintained by cross-rs. +- #792 - added `CROSS_CONTAINER_IN_CONTAINER` environment variable to replace `CROSS_DOCKER_IN_DOCKER`. +- #785 - added support for remote container engines through data volumes through setting the `CROSS_REMOTE` environment variable. also adds in utility commands to create and remove persistent data volumes. +- #782 - added `build-std` config option, which builds the rust standard library from source if enabled. +- #678 - Add optional `target.{target}.dockerfile[.file]`, `target.{target}.dockerfile.context` and `target.{target}.dockerfile.build-args` to invoke docker/podman build before using an image. +- #678 - Add `target.{target}.pre-build` config for running commands before building the image. +- #772 - added `CROSS_CONTAINER_OPTS` environment variable to replace `DOCKER_OPTS`. +- #767, #788 - added the `cross-util` and `xtask` commands. +- #842 - Add `Cargo.toml` as configuration source +- #745 - added `thumbv7neon-*` targets. +- #741 - added `armv7-unknown-linux-gnueabi` and `armv7-unknown-linux-musleabi` targets. +- #721 - add support for running doctests on nightly if `CROSS_UNSTABLE_ENABLE_DOCTESTS=true`. +- #719 - add `--list` to known subcommands. +- #681 - Warn on unknown fields and confusable targets +- #624 - Add `build.default-target` +- #647 - Add `mips64-unknown-linux-muslabi64` and `mips64el-unknown-linux-muslabi64` support +- #543 - Added environment variables to control the UID and GID in the container +- #524 - docker: Add Nix Store volume support +- Added support for mounting volumes. +- #684 - Enable cargo workspaces to work from any path in the workspace, and make path dependencies mount seamlessly. Also added support for private SSH dependencies. + +### Changed + +- #838 - re-enabled the solaris targets. +- #807 - update Qemu to 6.1.0 on images using Ubuntu 18.04+ with python3.6+. +- #775 - forward Cargo exit code to host +- #762 - re-enabled `x86_64-unknown-dragonfly` target. +- #747 - reduced android image sizes. +- #746 - limit image permissions for android images. +- #377 - update WINE versions to 7.0. +- #734 - patch `arm-unknown-linux-gnueabihf` to build for ARMv6, and add architecture for crosstool-ng-based images. +- #709 - Update Emscripten targets to `emcc` version 3.1.10 +- #707, #708 - Set `BINDGEN_EXTRA_CLANG_ARGS` environment variable to pass sysroot to `rust-bindgen` +- #696 - bump freebsd to 12.3 +- #629 - Update Android NDK version and API version +- #497 - don't set RUSTFLAGS in aarch64-musl image +- #492 - Add cmake to FreeBSD images +- #748 - allow definitions in the environment variable passthrough + +### Fixed + +- #836 - write a `CACHEDIR.TAG` when creating the target directory, similar to `cargo`. +- #804 - allow usage of env `CARGO_BUILD_TARGET` as an alias for `CROSS_BUILD_TARGET` +- #792 - fixed container-in-container support when using podman. +- #781 - ensure `target.$(...)` config options override `build` ones. +- #771 - fix parsing of `DOCKER_OPTS`. +- #727 - add `PKG_CONFIG_PATH` to all `*-linux-gnu` images. +- #722 - boolean environment variables are evaluated as truthy or falsey. +- #720 - add android runner to preload `libc++_shared.so`. +- #725 - support `CROSS_DEBUG` and `CROSS_RUNNER` on android images. +- #714 - use host target directory when falling back to host cargo. +- #713 - convert relative target directories to absolute paths. +- #501 (reverted, see #764) - x86_64-linux: lower glibc version requirement to 2.17 (compatible with centos 7) +- #500 - use runner setting specified in Cross.toml +- #498 - bump linux-image version to fix CI +- Re-enabled `powerpc64-unknown-linux-gnu` image +- Re-enabled `sparc64-unknown-linux-gnu` image +- #582 - Added `libprocstat.so` to FreeBSD images +- #665 - when not using [env.volumes](https://github.com/cross-rs/cross#mounting-volumes-into-the-build-environment), mount project in /project +- #494 - Parse Cargo's --manifest-path option to determine mounted docker root + +### Removed + +- #718 - remove deb subcommand. + +### Internal + +- #856 - remove use of external wslpath and create internal helper that properly handles UNC paths. +- #828 - assume paths are Unicode and provide better error messages for path encoding errors. +- #787 - add installer for git hooks. +- #786, #791 - Migrate build script to rust: `cargo build-docker-image $TARGET` +- #730 - make FreeBSD builds more resilient. +- #670 - Use serde for deserialization of Cross.toml +- Change rust edition to 2021 and bump MSRV for the cross binary to 1.58.1 +- #654 - Use color-eyre for error reporting +- #658 - Upgrade dependencies +- #652 - Allow trying individual targets via bors. +- #650 - Improve Docker caching. +- #609 - Switch to Github Actions and GHCR. +- #588 - fix ci: bump openssl version in freebsd again +- #552 - Added CHANGELOG.md automation +- #534 - fix image builds with update of dependencies +- #502 - fix ci: bump openssl version in freebsd +- #489 - Add support for more hosts and simplify/unify host support checks +- #477 - Fix Docker/Podman links in README +- #476 - Use Rustlang mirror for Sabotage linux tarbals +- Bump nix dependency to `0.22.1` +- Bump musl version to 1.1.24. + +## [v0.2.1] - 2020-06-30 + +- Disabled `powerpc64-unknown-linux-gnu` image. +- Disabled `sparc64-unknown-linux-gnu` image. +- Disabled `x86_64-unknown-dragonfly` image. +- Removed CI testing for `i686-apple-darwin`. + +## [v0.2.0] - 2020-02-22 + +- Removed OpenSSL from all images. +- Added support for Podman. +- Bumped all images to at least Ubuntu 16.04. + +## [v0.1.16] - 2019-09-17 + +- Bump OpenSSL version to 1.0.2t. +- Re-enabled `asmjs-unknown-emscripten` target. +- Default to `native` runner instead of `qemu-user` for certain targets. + +## [v0.1.15] - 2019-09-04 + +- Images are now hosted at https://hub.docker.com/r/rustembedded/cross. +- Bump OpenSSL version to 1.0.2p. +- Bump musl version to 1.1.20. +- Bump Ubuntu to 18.04 to all musl targets. +- Bump gcc version to 6.3.0 for all musl targets. +- OpenSSL support for the `arm-unknown-linux-musleabi` target. +- OpenSSL support for the `armv7-unknown-linux-musleabihf` target. +- Build and test support for `aarch64-unknown-linux-musl`, `arm-unknown-linux-musleabihf`, + `armv5te-unknown-linux-musleabi`, `i586-unknown-linux-musl`, `mips-unknown-linux-musl`, + add `mipsel-unknown-linux-musl` targets. + +## [v0.1.14] - 2017-11-22 + +### Added + +- Support for the `i586-unknown-linux-gnu` target. + +### Changed + +- Downgraded the Solaris toolchains from 2.11 to 2.10 to make the binaries produced by Cross more + compatible (this version matches what rust-lang/rust is using). + +## [v0.1.13] - 2017-11-08 + +### Added + +- Support for the custom [`deb`] subcommand. + +[`deb`]: https://github.com/mmstick/cargo-deb + +- Partial `test` / `run` support for android targets. Using the android API via `cross run` / `cross test` is _not_ supported because Cross is using QEMU instead of the official Android emulator. + +- Partial support for the `sparcv9-sun-solaris` and `x86_64-sun-solaris` targets. `cross test` and + `cross run` doesn't work for these new targets. + +- OpenSSL support for the `i686-unknown-linux-musl` target. + +### Changed + +- Bump OpenSSL version to 1.0.2m. + +## [v0.1.12] - 2017-09-22 + +### Added + +- Support for `cross check`. This subcommand won't use any Docker container. + +### Changed + +- `binfmt_misc` is not required on the host for toolchain v1.19.0 and newer. + With these toolchains `binfmt_misc` interpreters don't need to be installed + on the host saving a _privileged_ docker run which some systems don't allow. + +## [v0.1.11] - 2017-06-10 + +### Added + +- Build and test support for `i686-pc-windows-gnu`, `x86_64-pc-windows-gnu`, + `asmjs-unknown-emscripten` and `wasm-unknown-emscripten`. + +- Build support for `aarch64-linux-android`, `arm-linux-androideabi`, + `armv7-linux-androideabi`, `x86_64-linux-android` and `i686-linux-android` + +- A `build.env.passthrough` / `build.target.*.passthrough` option to Cross.toml + to support passing environment variables from the host to the Docker image. + +### Changed + +- Bumped OpenSSL version to 1.0.2k +- Bumped QEMU version to 2.9.0 + +## [v0.1.10] - 2017-04-02 + +### Added + +- Cross compilation support for `x86_64-pc-windows-gnu` + +- Cross compilation support for Android targets + +### Changed + +- Bumped OpenSSL version to 1.0.2k + +## [v0.1.9] - 2017-02-08 + +### Added + +- Support for ARM MUSL targets. + +### Changed + +- The automatic lockfile update that happens every time `cross` is invoked + should no longer hit the network when there's no git dependency to add/update. + +- The QEMU_STRACE variable is passed to the underlying Docker container. Paired + with `cross run`, this lets you get a trace of system call from the execution + of "foreign" (non x86_64) binaries. + +## [v0.1.8] - 2017-01-21 + +### Added + +- Support for custom targets. Cross will now also try to use a docker image for + them. As with the built-in targets, one can override the image using + `[target.{}.image]` in Cross.toml. + +### Changed + +- Moved to a newer Xargo: v0.3.5 + +## [v0.1.7] - 2017-01-19 + +### Changed + +- Moved to a newer Xargo: v0.3.4 + +### Fixed + +- QEMU interpreters were being register when not required, e.g. for the + `x86_64-unknown-linux-gnu` target. + +## [v0.1.6] - 2017-01-14 + +### Fixed + +- Stable releases were picking the wrong image (wrong tag: 0.1.5 instead of + v0.1.5) + +## [v0.1.5] - 2017-01-14 [YANKED] + +### Added + +- `cross run` support for the thumb targets. + +- A `build.xargo` / `target.$TARGET.xargo` option to Cross.toml to use Xargo + instead of Cargo. + +- A `target.$TARGET.image` option to override the Docker image used for + `$TARGET`. + +- A `sparc64-unknown-linux-gnu` environment. + +- A `x86_64-unknown-dragonfly` environment. + +### Changed + +- Building older versions (<0.7.0) of the `openssl` crate is now supported. + +- Before Docker is invoked, `cross` will _always_ (re)generate the lockfile to + avoid errors later on due to read/write permissions. This removes the need to + call `cargo generate-lockfile` before `cross` in _all_ cases. + +## [v0.1.4] - 2017-01-07 + +### Added + +- Support for the `arm-unknown-linux-gnueabi` target + +- `cross build` support for: + - `i686-unknown-freebsd` + - `x86_64-unknown-freebsd` + - `x86_64-unknown-netbsd` + +### Changed + +- It's no longer necessary to call `cargo generate-lockfile` before using + `cross` as `cross` will now take care of creating a lockfile when necessary. + +- The C environments for the `thumb` targets now include newlib (`libc.a`, + `libm.a`, etc.) + +### Fixed + +- A segfault when `cross` was trying to figure out the name of the user that + called it. + +## [v0.1.3] - 2017-01-01 + +### Changed + +- Fix the `i686-unknown-linux-musl` target + +## [v0.1.2] - 2016-12-31 + +### Added + +- Support for `i686-unknown-linux-musl` +- Support for `cross build`ing crates for the `thumbv*-none-eabi*` targets. + +## [v0.1.1] - 2016-12-28 + +### Added + +- Support for `x86_64-unknown-linux-musl` +- Print shell commands when the verbose flag is used. +- Support crossing from x86_64 osx to i686 osx + +## v0.1.0 - 2016-12-26 + +- Initial release. Supports 12 targets. + + + + +[Unreleased]: https://github.com/cross-rs/cross/compare/v0.2.2...HEAD + +[v0.2.2]: https://github.com/cross-rs/cross/compare/v0.2.1...v0.2.2 +[v0.2.1]: https://github.com/cross-rs/cross/compare/v0.2.0...v0.2.1 +[v0.2.0]: https://github.com/cross-rs/cross/compare/v0.1.16...v0.2.0 +[v0.1.16]: https://github.com/cross-rs/cross/compare/v0.1.15...v0.1.16 +[v0.1.15]: https://github.com/cross-rs/cross/compare/v0.1.14...v0.1.15 +[v0.1.14]: https://github.com/cross-rs/cross/compare/v0.1.13...v0.1.14 +[v0.1.13]: https://github.com/cross-rs/cross/compare/v0.1.12...v0.1.13 +[v0.1.12]: https://github.com/cross-rs/cross/compare/v0.1.11...v0.1.12 +[v0.1.11]: https://github.com/cross-rs/cross/compare/v0.1.10...v0.1.11 +[v0.1.10]: https://github.com/cross-rs/cross/compare/v0.1.9...v0.1.10 +[v0.1.9]: https://github.com/cross-rs/cross/compare/v0.1.8...v0.1.9 +[v0.1.8]: https://github.com/cross-rs/cross/compare/v0.1.7...v0.1.8 +[v0.1.7]: https://github.com/cross-rs/cross/compare/v0.1.6...v0.1.7 +[v0.1.6]: https://github.com/cross-rs/cross/compare/v0.1.5...v0.1.6 +[v0.1.5]: https://github.com/cross-rs/cross/compare/v0.1.4...v0.1.5 +[v0.1.4]: https://github.com/cross-rs/cross/compare/v0.1.3...v0.1.4 +[v0.1.3]: https://github.com/cross-rs/cross/compare/v0.1.2...v0.1.3 +[v0.1.2]: https://github.com/cross-rs/cross/compare/v0.1.1...v0.1.2 +[v0.1.1]: https://github.com/cross-rs/cross/compare/v0.1.0...v0.1.1 + diff --git a/.changes/template/issue440.json b/.changes/template/issue440.json new file mode 100644 index 000000000..33772138a --- /dev/null +++ b/.changes/template/issue440.json @@ -0,0 +1,5 @@ +{ + "description": "no associated PR.", + "issues": [440], + "type": "fixed" +} diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 24882f538..b52be1675 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -9,12 +9,35 @@ jobs: name: Changelog check runs-on: ubuntu-latest steps: - - name: Checkout sources - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + - uses: ./.github/actions/setup-rust - - name: Changelog updated - uses: Zomzog/changelog-checker@v1.2.0 + - name: Get Changed Files + id: files + uses: Ana06/get-changed-files@v2.1.0 with: - fileName: CHANGELOG.md - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # use JSON so we don't have to worry about filenames with spaces + format: 'json' + filter: '.changes/*.json' + + - name: Validate Changelog Files + id: changelog + run: | + set -x + set -e + readarray -t added_modified <<<"$(jq -r '.[]' <<<'${{ steps.files.outputs.added_modified }}')" + readarray -t removed <<<"$(jq -r '.[]' <<<'${{ steps.files.outputs.removed }}')" + added_count=${#added_modified[@]} + removed_count=${#removed[@]} + if ${{ !contains(github.event.pull_request.labels.*.name, 'no changelog' ) }}; then + if [[ "$added_count" -eq "0" ]] && [[ "$removed_count" -eq "0" ]]; then + echo "Must add or remove changes or add the 'no changelog' label" + exit 1 + else + basenames=() + for path in "${added_modified[@]}"; do + basenames+=($(basename "${path}")) + done + cargo xtask validate-changelog "${basenames[@]}" + fi + fi diff --git a/.gitignore b/.gitignore index e333188d5..d89dbc825 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ **/.idea/ **/.vscode/*.* **/*.log -/cargo-timing*.html \ No newline at end of file +/cargo-timing*.html +CHANGELOG.md.draft diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e6259955..c4906a2e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -All notable changes to this project will be documented in this file. +All notable changes to this project will be documented in this file. This is an automatically-generated document: entries are added via changesets present in the `.changes` directory. This project adheres to [Semantic Versioning](http://semver.org/). diff --git a/Cargo.lock b/Cargo.lock index 55d993317..439369521 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + [[package]] name = "clap" version = "3.2.5" @@ -249,7 +262,7 @@ checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -369,6 +382,25 @@ dependencies = [ "libc", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.28.4" @@ -687,6 +719,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "toml" version = "0.5.9" @@ -767,6 +810,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -819,6 +868,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" name = "xtask" version = "0.0.0-dev.0" dependencies = [ + "chrono", "clap", "color-eyre", "cross", diff --git a/Cargo.toml b/Cargo.toml index a7918d6f6..3ed1a2e46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,30 +70,15 @@ dev-version = false push = false publish = false tag = false +pre-release-hook = ["cargo", "xtask", "build-changelog"] pre-release-commit-message = "release version {{version}}" -[[package.metadata.release.pre-release-replacements]] -file = "CHANGELOG.md" -search = "Unreleased" -replace = "v{{version}}" - [[package.metadata.release.pre-release-replacements]] file = "CHANGELOG.md" search = "\\.\\.\\.HEAD" replace = "...v{{version}}" exactly = 1 -[[package.metadata.release.pre-release-replacements]] -file = "CHANGELOG.md" -search = "ReleaseDate" -replace = "{{date}}" - -[[package.metadata.release.pre-release-replacements]] -file = "CHANGELOG.md" -search = "" -replace = "\n\n## [Unreleased] - ReleaseDate" -exactly = 1 - [[package.metadata.release.pre-release-replacements]] file = "CHANGELOG.md" search = "" diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 8cafcedf8..825428bd4 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -20,3 +20,4 @@ serde_yaml = "0.8" serde_json = "1.0" once_cell = "1.12" semver = "1" +chrono = "0.4" diff --git a/xtask/src/changelog.rs b/xtask/src/changelog.rs new file mode 100644 index 000000000..61940242c --- /dev/null +++ b/xtask/src/changelog.rs @@ -0,0 +1,832 @@ +use std::cmp; +use std::fmt; +use std::fs; +use std::path::Path; + +use crate::util::{project_dir, write_to_string}; +use chrono::{Datelike, Utc}; +use clap::Args; +use cross::shell::MessageInfo; +use cross::ToUtf8; +use eyre::Context; +use serde::Deserialize; + +#[derive(Args, Debug)] +pub struct BuildChangelog { + /// Provide verbose diagnostic output. + #[clap(short, long, env = "CARGO_TERM_VERBOSE")] + pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long, env = "CARGO_TERM_QUIET")] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long, env = "CARGO_TERM_COLOR")] + pub color: Option, + /// Build a release changelog. + #[clap(long, env = "NEW_VERSION")] + release: Option, + /// Whether we're doing a dry run or not. + #[clap(long, env = "DRY_RUN")] + dry_run: bool, +} + +#[derive(Args, Debug)] +pub struct ValidateChangelog { + /// List of changelog entries to validate. + files: Vec, + /// Provide verbose diagnostic output. + #[clap(short, long)] + pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, +} + +// the type for the identifier: if it's a PR, sort +// by the number, otherwise, sort as 0. the numbers +// should be sorted, and the `max(values) || 0` should +// be used +#[derive(Debug, Clone, PartialEq, Eq)] +enum IdType { + PullRequest(Vec), + Issue(Vec), +} + +impl IdType { + fn numbers(&self) -> &[u64] { + match self { + IdType::PullRequest(v) => v, + IdType::Issue(v) => v, + } + } + + fn max_number(&self) -> u64 { + self.numbers().iter().max().map_or_else(|| 0, |v| *v) + } + + fn parse_stem(file_stem: &str) -> cross::Result { + let (is_issue, rest) = match file_stem.strip_prefix("issue") { + Some(n) => (true, n), + None => (false, file_stem), + }; + let mut numbers = rest + .split('-') + .map(|x| x.parse::()) + .collect::, _>>()?; + numbers.sort_unstable(); + + Ok(match is_issue { + false => IdType::PullRequest(numbers), + true => IdType::Issue(numbers), + }) + } + + fn parse_changelog(prs: &str) -> cross::Result { + let mut numbers = prs + .split(',') + .map(|x| x.trim().parse::()) + .collect::, _>>()?; + numbers.sort_unstable(); + + Ok(IdType::PullRequest(numbers)) + } +} + +impl cmp::PartialOrd for IdType { + fn partial_cmp(&self, other: &IdType) -> Option { + self.max_number().partial_cmp(&other.max_number()) + } +} + +impl cmp::Ord for IdType { + fn cmp(&self, other: &IdType) -> cmp::Ordering { + self.max_number().cmp(&other.max_number()) + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +enum ChangelogType { + Added, + Changed, + Fixed, + Removed, + Internal, +} + +impl ChangelogType { + fn from_header(s: &str) -> cross::Result { + Ok(match s { + "Added" => Self::Added, + "Changed" => Self::Changed, + "Fixed" => Self::Fixed, + "Removed" => Self::Removed, + "Internal" => Self::Internal, + _ => eyre::bail!("invalid header section, got {s}"), + }) + } + + fn sort_by(&self) -> u32 { + match self { + ChangelogType::Added => 4, + ChangelogType::Changed => 3, + ChangelogType::Fixed => 2, + ChangelogType::Removed => 1, + ChangelogType::Internal => 0, + } + } +} + +impl cmp::PartialOrd for ChangelogType { + fn partial_cmp(&self, other: &ChangelogType) -> Option { + self.sort_by().partial_cmp(&other.sort_by()) + } +} + +impl cmp::Ord for ChangelogType { + fn cmp(&self, other: &ChangelogType) -> cmp::Ordering { + self.sort_by().cmp(&other.sort_by()) + } +} + +// internal type for a changelog, just containing the contents +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +struct ChangelogContents { + description: String, + #[serde(default)] + issues: Vec, + #[serde(default)] + breaking: bool, + #[serde(rename = "type")] + kind: ChangelogType, +} + +impl ChangelogContents { + fn sort_by(&self) -> (&ChangelogType, &str, &bool) { + (&self.kind, &self.description, &self.breaking) + } +} + +impl cmp::PartialOrd for ChangelogContents { + fn partial_cmp(&self, other: &ChangelogContents) -> Option { + self.sort_by().partial_cmp(&other.sort_by()) + } +} + +impl cmp::Ord for ChangelogContents { + fn cmp(&self, other: &ChangelogContents) -> cmp::Ordering { + self.sort_by().cmp(&other.sort_by()) + } +} + +impl fmt::Display for ChangelogContents { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.breaking { + f.write_str("BREAKING: ")?; + } + f.write_str(&self.description) + } +} + +#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)] +struct ChangelogEntry { + id: IdType, + contents: ChangelogContents, +} + +impl ChangelogEntry { + fn new(id: IdType, contents: ChangelogContents) -> Self { + Self { id, contents } + } + + fn parse(s: &str, kind: ChangelogType) -> cross::Result { + let (id, rest) = match s.split_once('-') { + Some((prefix, rest)) => match prefix.trim().strip_prefix('#') { + Some(prs) => (IdType::parse_changelog(prs)?, rest), + None => (IdType::Issue(vec![]), s), + }, + None => (IdType::Issue(vec![]), s), + }; + + let trimmed = rest.trim(); + let (breaking, description) = match trimmed.strip_prefix("BREAKING: ") { + Some(d) => (true, d.trim().to_owned()), + None => (false, trimmed.to_owned()), + }; + + Ok(ChangelogEntry { + id, + contents: ChangelogContents { + kind, + breaking, + description, + issues: vec![], + }, + }) + } + + fn from_object(id: IdType, value: serde_json::Value) -> cross::Result { + Ok(Self::new(id, serde_json::value::from_value(value)?)) + } + + fn from_value(id: IdType, mut value: serde_json::Value) -> cross::Result> { + let mut result = vec![]; + if value.is_array() { + for item in value.as_array_mut().expect("must be array") { + result.push(Self::from_object(id.clone(), item.take())?); + } + } else { + result.push(Self::from_object(id, value)?); + } + + Ok(result) + } +} + +impl fmt::Display for ChangelogEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("-")?; + match &self.id { + IdType::PullRequest(prs) => f.write_fmt(format_args!( + " #{} -", + prs.iter() + .map(|x| x.to_string()) + .collect::>() + .join(",") + ))?, + IdType::Issue(_) => (), + } + f.write_fmt(format_args!(" {}", self.contents))?; + f.write_str("\n") + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +struct Changes { + added: Vec, + changed: Vec, + fixed: Vec, + removed: Vec, + internal: Vec, +} + +impl Changes { + fn sort_descending(&mut self) { + self.added.sort_by(|x, y| y.cmp(x)); + self.changed.sort_by(|x, y| y.cmp(x)); + self.fixed.sort_by(|x, y| y.cmp(x)); + self.removed.sort_by(|x, y| y.cmp(x)); + self.internal.sort_by(|x, y| y.cmp(x)); + } + + fn merge(&mut self, other: &mut Self) { + self.added.append(&mut other.added); + self.changed.append(&mut other.changed); + self.fixed.append(&mut other.fixed); + self.removed.append(&mut other.removed); + self.internal.append(&mut other.internal); + } + + fn push(&mut self, entry: ChangelogEntry) { + match entry.contents.kind { + ChangelogType::Added => self.added.push(entry), + ChangelogType::Changed => self.changed.push(entry), + ChangelogType::Fixed => self.fixed.push(entry), + ChangelogType::Removed => self.removed.push(entry), + ChangelogType::Internal => self.internal.push(entry), + } + } +} + +macro_rules! fmt_changelog_vec { + ($self:ident, $fmt:ident, $field:ident, $header:literal) => {{ + if !$self.$field.is_empty() { + $fmt.write_str(concat!("\n### ", $header, "\n\n"))?; + for entry in &$self.$field { + $fmt.write_fmt(format_args!("{}", entry))?; + } + } + }}; +} + +impl fmt::Display for Changes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt_changelog_vec!(self, f, added, "Added"); + fmt_changelog_vec!(self, f, changed, "Changed"); + fmt_changelog_vec!(self, f, fixed, "Fixed"); + fmt_changelog_vec!(self, f, removed, "Removed"); + fmt_changelog_vec!(self, f, internal, "Internal"); + + Ok(()) + } +} + +fn file_stem(path: &Path) -> cross::Result<&str> { + path.file_stem() + .ok_or(eyre::eyre!("unable to get file stem {path:?}"))? + .to_utf8() +} + +fn read_changes(changes_dir: &Path) -> cross::Result { + let mut changes = Changes::default(); + for entry in fs::read_dir(changes_dir)? { + let entry = entry?; + let file_type = entry.file_type()?; + let file_name = entry.file_name(); + let path = entry.path(); + let ext = path.extension(); + if file_type.is_file() && ext.map_or(false, |v| v == "json") { + let stem = file_stem(&path)?; + let id = IdType::parse_stem(stem)?; + let contents = fs::read_to_string(path)?; + let value = serde_json::from_str(&contents) + .wrap_err_with(|| format!("unable to parse JSON for {file_name:?}"))?; + let new_entries = ChangelogEntry::from_value(id, value) + .wrap_err_with(|| format!("unable to extract changelog from {file_name:?}"))?; + for change in new_entries { + match change.contents.kind { + ChangelogType::Added => changes.added.push(change), + ChangelogType::Changed => changes.changed.push(change), + ChangelogType::Fixed => changes.fixed.push(change), + ChangelogType::Removed => changes.removed.push(change), + ChangelogType::Internal => changes.internal.push(change), + } + } + } + } + + Ok(changes) +} + +fn read_changelog(root: &Path) -> cross::Result<(String, Changes, String)> { + let lines: Vec = fs::read_to_string(root.join("CHANGELOG.md"))? + .lines() + .map(ToOwned::to_owned) + .collect(); + + let next_index = lines + .iter() + .position(|x| x.trim().starts_with("## [Unreleased]")) + .ok_or(eyre::eyre!("could not find unreleased section"))?; + let (header, rest) = lines.split_at(next_index); + + // need to skip the first index since it's previously + // matched, and then just increment our split by 1. + let last_index = 1 + rest[1..] + .iter() + .position(|x| x.trim().starts_with("## ")) + .ok_or(eyre::eyre!("could not find the next release section"))?; + let (section, footer) = rest.split_at(last_index); + + // the unreleased should have the format: + // ## [Unreleased] - ReleaseDate + // + // ### Added + // + // - #905 - ... + let mut kind = None; + let mut changes = Changes::default(); + for line in section { + let line = line.trim(); + if let Some(header) = line.strip_prefix("### ") { + kind = Some(ChangelogType::from_header(header)?); + } else if let Some(entry) = line.strip_prefix("- ") { + match kind { + Some(kind) => changes.push(ChangelogEntry::parse(entry, kind)?), + None => eyre::bail!("changelog entry \"{line}\" without header"), + } + } else if !(line.is_empty() || line == "## [Unreleased] - ReleaseDate") { + eyre::bail!("invalid changelog entry, got \"{line}\""); + } + } + + Ok((header.join("\n"), changes, footer.join("\n"))) +} + +fn move_changes(root: &Path, version: &str) -> cross::Result<()> { + // move all files to the denoted version release + let src = root.join(".changes"); + let dst = src.join(Path::new(version)); + fs::create_dir_all(&dst)?; + + for entry in fs::read_dir(src)? { + let entry = entry?; + let file_type = entry.file_type()?; + let srcpath = entry.path(); + let dstpath = dst.join(entry.file_name()); + let ext = srcpath.extension(); + if file_type.is_file() && ext.map_or(false, |v| v == "json") { + fs::rename(srcpath, dstpath)?; + } + } + + Ok(()) +} + +/// Get the date as a year/month/day tuple. +pub fn get_current_date() -> String { + let utc = Utc::now(); + let date = utc.date(); + + format!("{}-{:0>2}-{}", date.year(), date.month(), date.day()) +} + +// used for internal testing +fn build_changelog_from_dir( + root: &Path, + changes_dir: &Path, + release: Option<&str>, +) -> cross::Result { + use std::fmt::Write; + + let mut new = read_changes(changes_dir)?; + let (header, mut existing, footer) = read_changelog(root)?; + new.merge(&mut existing); + new.sort_descending(); + + let mut output = header; + output.push_str("\n## [Unreleased] - ReleaseDate\n"); + if let Some(release) = release { + let date = get_current_date(); + writeln!(&mut output, "\n## [v{release}] - {date}")?; + } + output.push_str(&new.to_string()); + output.push('\n'); + output.push_str(&footer); + + Ok(output) +} + +pub fn build_changelog( + BuildChangelog { + dry_run, release, .. + }: BuildChangelog, + msg_info: &mut MessageInfo, +) -> cross::Result<()> { + msg_info.info("Building the changelog.")?; + msg_info.debug(format_args!( + "Running with dry-run set the {dry_run} and with release {release:?}" + ))?; + + let root = project_dir(msg_info)?; + let changes_dir = root.join(".changes"); + let output = build_changelog_from_dir(&root, &changes_dir, release.as_deref())?; + + let filename = match (dry_run, release) { + (false, Some(version)) => { + move_changes(&root, &version)?; + "CHANGELOG.md" + } + _ => "CHANGELOG.md.draft", + }; + write_to_string(&root.join(filename), &output)?; + + Ok(()) +} + +pub fn validate_changelog( + ValidateChangelog { files, .. }: ValidateChangelog, + msg_info: &mut MessageInfo, +) -> cross::Result<()> { + msg_info.info("Validating the changelog modifications.")?; + + let root = project_dir(msg_info)?; + let changes_dir = root.join(".changes"); + for file in files { + let file_name = Path::new(&file); + let path = changes_dir.join(file_name); + let stem = file_stem(&path)?; + let contents = + fs::read_to_string(&path).wrap_err_with(|| eyre::eyre!("cannot find file {file}"))?; + let id = IdType::parse_stem(stem)?; + let value = serde_json::from_str(&contents) + .wrap_err_with(|| format!("unable to parse JSON for \"{file}\""))?; + let _ = ChangelogEntry::from_value(id, value) + .wrap_err_with(|| format!("unable to extract changelog from \"{file}\""))?; + } + + // also need to validate the existing changelog + let _ = read_changelog(&root)?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! s { + ($x:literal) => { + $x.to_owned() + }; + } + + #[test] + fn test_id_type_parse_stem() -> cross::Result<()> { + assert_eq!(IdType::parse_stem("645")?, IdType::PullRequest(vec![645])); + assert_eq!( + IdType::parse_stem("640-645")?, + IdType::PullRequest(vec![640, 645]) + ); + assert_eq!( + IdType::parse_stem("issue640-645")?, + IdType::Issue(vec![640, 645]) + ); + + Ok(()) + } + + #[test] + fn test_id_type_parse_changelog() -> cross::Result<()> { + assert_eq!( + IdType::parse_changelog("645")?, + IdType::PullRequest(vec![645]) + ); + assert_eq!( + IdType::parse_changelog("640,645")?, + IdType::PullRequest(vec![640, 645]) + ); + + Ok(()) + } + + #[test] + fn changelog_type_sort() { + assert!(ChangelogType::Added > ChangelogType::Changed); + assert!(ChangelogType::Changed > ChangelogType::Fixed); + } + + #[test] + fn change_log_type_from_header() -> cross::Result<()> { + assert_eq!(ChangelogType::from_header("Added")?, ChangelogType::Added); + + Ok(()) + } + + #[test] + fn changelog_contents_deserialize() -> cross::Result<()> { + let actual: ChangelogContents = serde_json::from_str(CHANGES_OBJECT)?; + let expected = ChangelogContents { + description: s!("sample description for a PR adding one CHANGELOG entry."), + issues: vec![437], + breaking: false, + kind: ChangelogType::Fixed, + }; + assert_eq!(actual, expected); + + let actual: Vec = serde_json::from_str(CHANGES_ARRAY)?; + let expected = vec![ + ChangelogContents { + description: s!("this is one added entry."), + issues: vec![630], + breaking: false, + kind: ChangelogType::Added, + }, + ChangelogContents { + description: s!("this is another added entry."), + issues: vec![642], + breaking: false, + kind: ChangelogType::Added, + }, + ChangelogContents { + description: s!("this is a fixed entry that has no attached issue."), + issues: vec![], + breaking: false, + kind: ChangelogType::Fixed, + }, + ChangelogContents { + description: s!("this is a breaking change."), + issues: vec![679], + breaking: true, + kind: ChangelogType::Changed, + }, + ]; + assert_eq!(actual, expected); + + Ok(()) + } + + #[test] + fn changelog_entry_display() { + let mut entry = ChangelogEntry::new( + IdType::PullRequest(vec![637]), + ChangelogContents { + description: s!("this is one added entry."), + issues: vec![630], + breaking: false, + kind: ChangelogType::Added, + }, + ); + assert_eq!(entry.to_string(), s!("- #637 - this is one added entry.\n")); + + entry.contents.breaking = true; + assert_eq!( + entry.to_string(), + s!("- #637 - BREAKING: this is one added entry.\n") + ); + + entry.id = IdType::Issue(vec![640]); + assert_eq!( + entry.to_string(), + s!("- BREAKING: this is one added entry.\n") + ); + + entry.contents.breaking = false; + assert_eq!(entry.to_string(), s!("- this is one added entry.\n")); + } + + #[test] + fn read_template_changes() -> cross::Result<()> { + let mut msg_info = MessageInfo::default(); + let root = project_dir(&mut msg_info)?; + + let mut actual = read_changes(&root.join(".changes").join("template"))?; + actual.sort_descending(); + let expected = Changes { + added: vec![ + ChangelogEntry::new( + IdType::PullRequest(vec![979, 981]), + ChangelogContents { + description: s!("this has 2 PRs associated."), + issues: vec![441], + breaking: false, + kind: ChangelogType::Added, + }, + ), + ChangelogEntry::new( + IdType::PullRequest(vec![940]), + ChangelogContents { + description: s!("this is one added entry."), + issues: vec![630], + breaking: false, + kind: ChangelogType::Added, + }, + ), + ChangelogEntry::new( + IdType::PullRequest(vec![940]), + ChangelogContents { + description: s!("this is another added entry."), + issues: vec![642], + breaking: false, + kind: ChangelogType::Added, + }, + ), + ], + changed: vec![ChangelogEntry::new( + IdType::PullRequest(vec![940]), + ChangelogContents { + description: s!("this is a breaking change."), + issues: vec![679], + breaking: true, + kind: ChangelogType::Changed, + }, + )], + fixed: vec![ + ChangelogEntry::new( + IdType::PullRequest(vec![978]), + ChangelogContents { + description: s!("sample description for a PR adding one CHANGELOG entry."), + issues: vec![437], + breaking: false, + kind: ChangelogType::Fixed, + }, + ), + ChangelogEntry::new( + IdType::PullRequest(vec![940]), + ChangelogContents { + description: s!("this is a fixed entry that has no attached issue."), + issues: vec![], + breaking: false, + kind: ChangelogType::Fixed, + }, + ), + ChangelogEntry::new( + IdType::Issue(vec![440]), + ChangelogContents { + description: s!("no associated PR."), + issues: vec![440], + breaking: false, + kind: ChangelogType::Fixed, + }, + ), + ], + removed: vec![], + internal: vec![], + }; + assert_eq!(actual, expected); + + Ok(()) + } + + #[test] + fn read_template_changelog() -> cross::Result<()> { + let mut msg_info = MessageInfo::default(); + let root = project_dir(&mut msg_info)?; + + let (_, mut actual, _) = read_changelog(&root.join(".changes").join("template"))?; + actual.sort_descending(); + let expected = ChangelogEntry::new( + IdType::PullRequest(vec![905]), + ChangelogContents { + description: s!("added qemu emulation to `i586-unknown-linux-gnu`, `i686-unknown-linux-musl`, and `i586-unknown-linux-gnu`, so they can run on an `x86` CPU, rather than an `x86_64` CPU."), + issues: vec![], + breaking: false, + kind: ChangelogType::Added, + }, + ); + assert_eq!(actual.added[0], expected); + + let expected = ChangelogEntry::new( + IdType::PullRequest(vec![869]), + ChangelogContents { + description: s!("ensure cargo configuration environment variable flags are passed to the docker container."), + issues: vec![], + breaking: false, + kind: ChangelogType::Changed, + }, + ); + assert_eq!(actual.changed[0], expected); + + let expected = ChangelogEntry::new( + IdType::PullRequest(vec![905]), + ChangelogContents { + description: s!("fixed running dynamically-linked libraries for all musl targets except `x86_64-unknown-linux-musl`."), + issues: vec![], + breaking: false, + kind: ChangelogType::Fixed, + }, + ); + assert_eq!(actual.fixed[0], expected); + assert_eq!(actual.removed.len(), 0); + assert_eq!(actual.internal.len(), 0); + + Ok(()) + } + + #[test] + fn test_build_changelog() -> cross::Result<()> { + let mut msg_info = MessageInfo::default(); + let root = project_dir(&mut msg_info)?; + let changes_dir = root.join(".changes").join("template"); + + let output = build_changelog_from_dir(&changes_dir, &changes_dir, None)?; + let lines: Vec<&str> = output.lines().collect(); + + assert_eq!(lines[10], "- #979,981 - this has 2 PRs associated."); + assert_eq!(lines[11], "- #940 - this is one added entry."); + assert_eq!( + lines[36], + "- #885 - handle symlinks when using remote docker." + ); + assert_eq!(lines[39], "- no associated PR."); + assert_eq!( + &lines[6..12], + &[ + "## [Unreleased] - ReleaseDate", + "", + "### Added", + "", + "- #979,981 - this has 2 PRs associated.", + "- #940 - this is one added entry.", + ] + ); + + Ok(()) + } + + static CHANGES_OBJECT: &str = r#" + { + "description": "sample description for a PR adding one CHANGELOG entry.", + "issues": [437], + "type": "fixed" + } + "#; + + static CHANGES_ARRAY: &str = r#" + [ + { + "description": "this is one added entry.", + "issues": [630], + "type": "added" + }, + { + "description": "this is another added entry.", + "issues": [642], + "type": "added" + }, + { + "description": "this is a fixed entry that has no attached issue.", + "type": "fixed" + }, + { + "description": "this is a breaking change.", + "issues": [679], + "breaking": true, + "type": "changed" + } + ] + "#; +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index d2c68e211..9da2df92b 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,6 +1,7 @@ #![deny(missing_debug_implementations, rust_2018_idioms)] pub mod build_docker_image; +pub mod changelog; pub mod ci; pub mod crosstool; pub mod hooks; @@ -15,6 +16,7 @@ use cross::shell::{MessageInfo, Verbosity}; use util::{cargo_metadata, ImageTarget}; use self::build_docker_image::BuildDockerImage; +use self::changelog::{BuildChangelog, ValidateChangelog}; use self::crosstool::ConfigureCrosstool; use self::hooks::{Check, Test}; use self::install_git_hooks::InstallGitHooks; @@ -54,6 +56,11 @@ enum Commands { CiJob(CiJob), /// Configure crosstool config files. ConfigureCrosstool(ConfigureCrosstool), + /// Build the changelog. + BuildChangelog(BuildChangelog), + /// Validate changelog entries. + #[clap(hide = true)] + ValidateChangelog(ValidateChangelog), } fn is_toolchain(toolchain: &str) -> cross::Result { @@ -111,6 +118,14 @@ pub fn main() -> cross::Result<()> { let mut msg_info = get_msg_info!(args, args.verbose)?; crosstool::configure_crosstool(args, &mut msg_info)?; } + Commands::BuildChangelog(args) => { + let mut msg_info = get_msg_info!(args, args.verbose)?; + changelog::build_changelog(args, &mut msg_info)?; + } + Commands::ValidateChangelog(args) => { + let mut msg_info = get_msg_info!(args, args.verbose)?; + changelog::validate_changelog(args, &mut msg_info)?; + } } Ok(())