diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 000000000..6ecba2382 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,45 @@ +name: Create release + +on: + push: + tags: + - "v*" + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Rust nightly + uses: ./.forgejo/actions/setup-rust + with: + rust-version: nightly + + - name: Setup towncrier + run: uv tool install towncrier + + - name: Generate release notes + run: cargo run --package xtask --no-default-features -- generate-release-notes > /tmp/RELEASE.md + + - name: Create release + run: | + set -euo pipefail + + RELEASE_BODY='{"body": $body, "tag_name": "${{ forgejo.ref_name }}", "name": "${{ forgejo.ref_name }}"}' + + jq -n --rawfile body RELEASE.md $RELEASE_BODY \ + | curl --fail-with-body \ + --json @- \ + -X POST \ + https://webhook.site/933d73fa-9c2a-48db-b201-0391f6d7f9eb + + - name: Remove news fragments + run: git rm -r changelog.d/ + + - name: Commit changes + run: | + git commit -am "chore: Post-release cleanup" + + git push origin HEAD:refs/heads/main diff --git a/Cargo.lock b/Cargo.lock index 976ea808c..aa851f707 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1671,6 +1671,18 @@ version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" +[[package]] +name = "duct" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e66e9c0c03d094e1a0ba1be130b849034aa80c3a2ab8ee94316bc809f3fa684" +dependencies = [ + "libc", + "os_pipe", + "shared_child", + "shared_thread", +] + [[package]] name = "dunce" version = "1.0.5" @@ -3374,6 +3386,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "parking" version = "2.2.1" @@ -4797,12 +4819,40 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shared_thread" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0" + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + [[package]] name = "signal-hook" version = "0.3.18" @@ -6223,6 +6273,7 @@ dependencies = [ "clap", "conduwuit", "conduwuit_admin", + "duct", ] [[package]] diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index b43bfdb1e..9b8fe21c9 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -9,13 +9,18 @@ readme.workspace = true repository.workspace = true version.workspace = true +[features] +default = ["generate-docs"] +generate-docs = ["dep:conduwuit", "dep:conduwuit-admin"] + [dependencies] -conduwuit-admin.workspace = true -conduwuit.workspace = true clap.workspace = true +conduwuit = { workspace = true, optional = true } +conduwuit-admin = { workspace = true, optional = true } askama = "0.15.1" cargo_metadata = "0.23.1" +duct = "1.1.1" [lints] workspace = true diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 19e96fac7..4b253c216 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,5 +1,6 @@ mod tasks; +use cargo_metadata::MetadataCommand; use clap::Parser; use crate::tasks::Task; @@ -22,5 +23,10 @@ struct Args { fn main() -> impl std::process::Termination { let BaseArgs { task, args } = BaseArgs::parse(); - task.invoke(args) + let metadata = MetadataCommand::new() + .no_deps() + .exec() + .expect("should have been able to run cargo"); + + task.invoke(metadata, args) } diff --git a/xtask/src/tasks/generate_docs/mod.rs b/xtask/src/tasks/generate_docs/mod.rs index 5da1651ce..ddc33ca9e 100644 --- a/xtask/src/tasks/generate_docs/mod.rs +++ b/xtask/src/tasks/generate_docs/mod.rs @@ -2,8 +2,6 @@ mod admin_commands; use std::{collections::HashMap, fs::File, io::Write, path::{Path, PathBuf}}; -use cargo_metadata::MetadataCommand; - use crate::tasks::TaskResult; trait FileOutput { @@ -68,14 +66,9 @@ pub(crate) struct Args { } #[expect(clippy::needless_pass_by_value)] -pub(super) fn run(common_args: crate::Args, task_args: Args) -> TaskResult<()> { +pub(super) fn run(metadata: cargo_metadata::Metadata, common_args: crate::Args, task_args: Args) -> TaskResult<()> { let mut queue = FileQueue::default(); - let metadata = MetadataCommand::new() - .no_deps() - .exec() - .expect("should have been able to run cargo"); - let root = task_args.root.unwrap_or_else(|| metadata.workspace_root.join_os("docs/")); admin_commands::generate(&mut queue)?; diff --git a/xtask/src/tasks/generate_release_notes/mod.rs b/xtask/src/tasks/generate_release_notes/mod.rs new file mode 100644 index 000000000..35e329532 --- /dev/null +++ b/xtask/src/tasks/generate_release_notes/mod.rs @@ -0,0 +1,57 @@ +use askama::Template; +use duct::cmd; + +use crate::tasks::TaskResult; + +#[derive(askama::Template)] +#[template(path = "release-notes.md")] +struct ReleaseNotes<'a> { + version: &'a str, + header: &'a str, + changelog: &'a str, +} + +#[derive(clap::Args)] +pub(crate) struct Args; + +pub(super) fn run(_: cargo_metadata::Metadata, _: crate::Args, _: Args) -> TaskResult<()> { + const TAG_PREFIX: &str = "v"; + + let tag = cmd!("git", "describe", "--exact") + .stdout_capture() + .read() + .expect("failed to get current tag"); + + let version = tag + .strip_prefix(TAG_PREFIX) + .expect("tag did not start with expected prefix"); + + eprintln!("Generating release notes for {version}"); + + let header = cmd!("git", "tag", "-l", &tag, "--format", "%(contents)") + .stdout_capture() + .read() + .expect("failed to read tag contents"); + + let mut changelog = cmd!("towncrier", "build", "--draft", "--version", &version) + .stdout_capture() + .stderr_null() + .read() + .expect("failed to run towncrier"); + + + // towncrier generates a title with the project name and date, which we don't want. remove it here. + // split off the first line and make sure it's a markdown heading + assert!(changelog.starts_with("# "), "expected h1 at the start of towncrier output"); + let changelog = changelog.split_off(changelog.find("\n\n").unwrap()); + + let rendered = ReleaseNotes { + version, + header: header.trim(), + changelog: changelog.trim() + }.render()?; + + println!("{rendered}"); + + Ok(()) +} diff --git a/xtask/src/tasks/mod.rs b/xtask/src/tasks/mod.rs index 68142beaf..913d0b754 100644 --- a/xtask/src/tasks/mod.rs +++ b/xtask/src/tasks/mod.rs @@ -1,29 +1,33 @@ type TaskResult = Result>; -#[macro_export] macro_rules! tasks { ( $( - $module:ident: $desc:literal + $(#[$meta:meta])? $module:ident: $desc:literal ),* ) => { - $(pub(super) mod $module;)* + $( + $(#[cfg($meta)])? + pub(super) mod $module; + )* #[derive(clap::Subcommand)] #[allow(non_camel_case_types)] pub(super) enum Task { $( + $(#[cfg($meta)])? #[clap(about = $desc, long_about = None)] $module($module::Args), )* } impl Task { - pub(super) fn invoke(self, common_args: $crate::Args) -> TaskResult { + pub(super) fn invoke(self, metadata: cargo_metadata::Metadata, common_args: $crate::Args) -> TaskResult { match self { $( + $(#[cfg($meta)])? Self::$module(task_args) => { - $module::run(common_args, task_args) + $module::run(metadata, common_args, task_args) }, )* } @@ -33,5 +37,7 @@ macro_rules! tasks { } tasks! { - generate_docs: "Generate various documentation files. This is run automatically when compiling the website." + #[feature = "generate-docs"] + generate_docs: "Generate various documentation files. This is run automatically when compiling the website.", + generate_release_notes: "Generate release notes from towncrier and a git tag. Used by CI." } diff --git a/xtask/templates/release-notes.md b/xtask/templates/release-notes.md new file mode 100644 index 000000000..87fc2d365 --- /dev/null +++ b/xtask/templates/release-notes.md @@ -0,0 +1,35 @@ +# Continuwuity {{ version }} + +{{ header }} + +{{ changelog }} + +--- + +## Get Continuwuity + +### Docker Images +- Forgejo: `forgejo.ellis.link/continuwuation/continuwuity:v{{ version }}` + +Mirrors (may be outdated): +- Docker Hub: `docker.io/continuwuity/continuwuity:v{{ version }}` +- GitHub: `ghcr.io/continuwuity/continuwuity:v{{ version }}` +- GitLab: `registry.gitlab.com/continuwuity/continuwuity:v{{ version }}` + +You can also use the `latest` tag for the most recent release. + +### First-party Packages + +- [Debian packages](https://forgejo.ellis.link/continuwuation/-/packages/debian/continuwuity/) +- [Fedora RPM packages](https://forgejo.ellis.link/continuwuation/-/packages/rpm/continuwuity/) + +### Community Packages + +[![Packaging status](https://repology.org/badge/vertical-allrepos/continuwuity.svg?minversion={{ version }})](https://repology.org/project/continuwuity/versions) + +### Binaries + +Plain binaries are distributed below. They require glibc and liburing to work - most distributions already have these installed. + +### From Source +See our [deployment documentation](https://continuwuity.org/deploying.html) for instructions on building from source.