Compare commits

..

10 Commits

Author SHA1 Message Date
Ellis Git f7bc6db2d5 chore: Post-release cleanup 2026-01-12 22:03:18 +00:00
Ginger f65e0a83de ci: Fix 2026-01-12 17:00:46 -05:00
Ginger 290269c8a3 ci: Create xtask and workflow for release automation 2026-01-12 14:42:07 -05:00
Ginger 0269f3307d ci: Remove test workflow 2026-01-12 12:33:31 -05:00
Ginger 9e2e6941e2 ci: Fix tag 2026-01-12 12:32:00 -05:00
Ginger fd06bca825 ci: Add dispatch test workflow 2026-01-12 12:32:00 -05:00
Ginger f3cb13802a chore: Kick forgejo 2026-01-12 12:32:00 -05:00
Jade Ellis 4c796029bb chore: Add correct configuration for cargo release 2026-01-12 16:20:38 +00:00
Jade Ellis fc3615c46b docs: Changelog 2026-01-12 16:20:38 +00:00
Jade Ellis 7375f7a68e feat: Improve the display of the configuration in the admin room 2026-01-12 16:20:38 +00:00
24 changed files with 311 additions and 132 deletions
-57
View File
@@ -1,57 +0,0 @@
on:
workflow_dispatch:
inputs:
logLevel:
description: 'Log Level'
required: true
default: 'warning'
type: choice
options:
- info
- warning
- debug
tags:
description: 'Test scenario tags'
required: false
type: boolean
boolean_default_true:
description: 'Test scenario tags'
required: true
type: boolean
default: true
boolean_default_false:
description: 'Test scenario tags'
required: false
type: boolean
default: false
number1_default:
description: 'Number w. default'
default: '100'
type: number
number2:
description: 'Number w/o. default'
type: number
string1_default:
description: 'String w. default'
default: 'Hello world'
type: string
string2:
description: 'String w/o. default'
required: true
type: string
jobs:
test:
runs-on: docker
container:
image: code.forgejo.org/oci/debian:trixie
options: "--volume /srv/example:/srv/example"
steps:
- name: save and display context
run: |
d=/srv/example/workflow-dispatch/contexts/$FORGEJO_EVENT_NAME
mkdir -p $d
tee $d/github <<'EOF'
${{ toJSON(github) }}
EOF
+52
View File
@@ -0,0 +1,52 @@
name: Create release
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: "main"
fetch-depth: 0
- 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
jq -n --rawfile body /tmp/RELEASE.md '{"body": $body, "tag_name": "${{ forgejo.ref_name }}", "name": "${{ forgejo.ref_name }}"}' \
| curl --fail-with-body \
--data-binary @- \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--header "Authorization: token ${{ forgejo.token }}" \
-X POST \
"${{ forgejo.server_url }}/api/v1/repos/${{ forgejo.repository }}/releases"
- name: Remove news fragments
run: git rm -r changelog.d/
- name: Commit changes
run: |
git config user.email "forgejo@mail.ellis.link"
git config user.name "Ellis Git"
git commit -am "chore: Post-release cleanup"
git push origin HEAD:refs/heads/main
Generated
+51
View File
@@ -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]]
-2
View File
@@ -13,8 +13,6 @@ license = "Apache-2.0"
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
version = "0.5.2"
# Do not allow publishing, we handle that ourselves
publish = false
[workspace.metadata.crane]
name = "conduwuit"
+2 -4
View File
@@ -1759,10 +1759,6 @@
#
#config_reload_signal = true
# This item is undocumented. Please contribute documentation for it.
#
#ldap = false
[global.tls]
# Path to a valid TLS certificate file.
@@ -1930,6 +1926,8 @@
#
#admin_filter = ""
[global.antispam]
[global.antispam.meowlnir]
# The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
+7
View File
@@ -1 +1,8 @@
tag-message = "chore: Release v{{version}}"
tag-prefix = ""
shared-version = true
publish = false
sign-commit = true
sign-tag = true
-1
View File
@@ -6,7 +6,6 @@ license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
publish.workspace = true
[lib]
path = "mod.rs"
-1
View File
@@ -6,7 +6,6 @@ license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
publish.workspace = true
[lib]
path = "mod.rs"
-1
View File
@@ -6,7 +6,6 @@ license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
publish.workspace = true
build = "build.rs"
-1
View File
@@ -6,7 +6,6 @@ license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
publish.workspace = true
[lib]
path = "mod.rs"
+11 -6
View File
@@ -53,8 +53,7 @@
### For more information, see:
### https://continuwuity.org/configuration.html
"#,
ignore = "config_paths catchall well_known tls blurhashing \
allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure antispam"
ignore = "config_paths catchall"
)]
pub struct Config {
// Paths to config file(s). Not supposed to be set manually in the config file,
@@ -105,7 +104,7 @@ pub struct Config {
#[serde(default = "default_port")]
port: ListeningPort,
// external structure; separate section
/// display: nested
#[serde(default)]
pub tls: TlsConfig,
@@ -724,7 +723,7 @@ pub struct Config {
#[serde(default = "default_default_room_version")]
pub default_room_version: RoomVersionId,
// external structure; separate section
/// display: nested
#[serde(default)]
pub well_known: WellKnownConfig,
@@ -2030,19 +2029,22 @@ pub struct Config {
/// etc. This is a hidden argument that should NOT be used in production as
/// it is highly insecure and I will personally yell at you if I catch you
/// using this.
///
/// display: hidden
#[serde(default)]
pub allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure:
bool,
// external structure; separate section
/// display: nested
#[serde(default)]
pub ldap: LdapConfig,
/// Configuration for antispam support
/// display: nested
#[serde(default)]
pub antispam: Option<Antispam>,
// external structure; separate section
/// display: nested
#[serde(default)]
pub blurhashing: BlurhashConfig,
#[serde(flatten)]
@@ -2259,8 +2261,11 @@ struct ListeningAddr {
}
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.antispam")]
pub struct Antispam {
/// display: nested
pub meowlnir: Option<MeowlnirConfig>,
/// display: nested
pub draupnir: Option<DraupnirConfig>,
}
-1
View File
@@ -6,7 +6,6 @@ license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
publish.workspace = true
[lib]
path = "mod.rs"
-1
View File
@@ -6,7 +6,6 @@ license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
publish.workspace = true
[lib]
name = "conduwuit_macros"
+69 -35
View File
@@ -78,6 +78,8 @@ fn generate_example(input: &ItemStruct, args: &[Meta], write: bool) -> Result<To
}
let mut summary: Vec<TokenStream2> = Vec::new();
let mut nested_displays: Vec<TokenStream2> = Vec::new();
if let Fields::Named(FieldsNamed { named, .. }) = &input.fields {
for field in named {
let Some(ident) = &field.ident else {
@@ -92,35 +94,6 @@ fn generate_example(input: &ItemStruct, args: &[Meta], write: bool) -> Result<To
continue;
};
let doc = get_doc_comment(field)
.unwrap_or_else(|| undocumented.into())
.trim_end()
.to_owned();
let doc = if doc.ends_with('#') {
format!("{doc}\n")
} else {
format!("{doc}\n#\n")
};
let default = get_doc_comment_line(field, "default")
.or_else(|| get_default(field))
.unwrap_or_default();
let default = if !default.is_empty() {
format!(" {default}")
} else {
default
};
if let Some(file) = file.as_mut() {
file.write_fmt(format_args!("\n{doc}"))
.expect("written to config file");
file.write_fmt(format_args!("#{ident} ={default}\n"))
.expect("written to config file");
}
let display = get_doc_comment_line(field, "display");
let display_directive = |key| {
display
@@ -129,17 +102,77 @@ fn generate_example(input: &ItemStruct, args: &[Meta], write: bool) -> Result<To
.flat_map(|display| display.split(' '))
.any(|directive| directive == key)
};
let is_nested = display_directive("nested");
let is_hidden = display_directive("hidden");
if !display_directive("hidden") {
let value = if display_directive("sensitive") {
quote! { "***********" }
// Only generate config file entries for non-nested, visible types
if !is_nested && !is_hidden {
let doc = get_doc_comment(field)
.unwrap_or_else(|| undocumented.into())
.trim_end()
.to_owned();
let doc = if doc.ends_with('#') {
format!("{doc}\n")
} else {
quote! { format_args!("{:?}", self.#ident) }
format!("{doc}\n#\n")
};
let name = ident.to_string();
let default = get_doc_comment_line(field, "default")
.or_else(|| get_default(field))
.unwrap_or_default();
let default = if !default.is_empty() {
format!(" {default}")
} else {
default
};
if let Some(file) = file.as_mut() {
file.write_fmt(format_args!("\n{doc}"))
.expect("written to config file");
file.write_fmt(format_args!("#{ident} ={default}\n"))
.expect("written to config file");
}
}
// Generate Display implementation for all fields
let name = ident.to_string();
if display_directive("sensitive") {
summary.push(quote! {
writeln!(out, "| {} | {} |", #name, #value)?;
writeln!(out, "| {} | {} |", #name, "***********")?;
});
} else if is_nested {
let is_option = matches!(type_name.as_str(), "Option");
if is_option {
summary.push(quote! {
writeln!(out, "| {} | {} |", #name,
if self.#ident.is_some() { "[configured]" } else { "None" })?;
});
nested_displays.push(quote! {
if let Some(nested) = &self.#ident {
writeln!(out)?;
writeln!(out, "## {}", #name)?;
write!(out, "{}", nested)?;
}
});
} else {
summary.push(quote! {
writeln!(out, "| {} | [configured] |", #name)?;
});
nested_displays.push(quote! {
writeln!(out)?;
writeln!(out, "## {}", #name)?;
write!(out, "{}", &self.#ident)?;
});
}
} else {
summary.push(quote! {
writeln!(out, "| {} | {:?} |", #name, self.#ident)?;
});
}
}
@@ -159,6 +192,7 @@ fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(out, "| name | value |")?;
writeln!(out, "| :--- | :--- |")?;
#( #summary )*
#( #nested_displays )*
Ok(())
}
}
-1
View File
@@ -9,7 +9,6 @@ license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
publish.workspace = true
metadata.crane.workspace = true
[lib]
-1
View File
@@ -6,7 +6,6 @@ license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
publish.workspace = true
[lib]
path = "mod.rs"
-1
View File
@@ -6,7 +6,6 @@ license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
publish.workspace = true
[lib]
path = "mod.rs"
-1
View File
@@ -6,7 +6,6 @@ license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
publish.workspace = true
[lib]
path = "mod.rs"
+7 -3
View File
@@ -8,15 +8,19 @@ license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
publish.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
+7 -1
View File
@@ -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)
}
+1 -8
View File
@@ -2,8 +2,6 @@
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)?;
@@ -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(())
}
+12 -6
View File
@@ -1,29 +1,33 @@
type TaskResult<T> = Result<T, Box<dyn std::error::Error>>;
#[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<impl std::process::Termination> {
pub(super) fn invoke(self, metadata: cargo_metadata::Metadata, common_args: $crate::Args) -> TaskResult<impl std::process::Termination> {
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 @@ pub(super) fn invoke(self, common_args: $crate::Args) -> TaskResult<impl std::pr
}
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."
}
+35
View File
@@ -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.