Compare commits

...
5 changed files with 112 additions and 19 deletions
+33 -1
View File
@@ -11859,7 +11859,39 @@
},
"process_count": {"$ref": "#/components/schemas/Int32Type"},
"process_limit": {"$ref": "#/components/schemas/Int32Type"},
"uptime_seconds": {"$ref": "#/components/schemas/Int32Type"}
"uptime_seconds": {"$ref": "#/components/schemas/Int32Type"},
"cluster_metrics": {
"type": "object",
"properties": {
"gateway_cluster_member_count": {"$ref": "#/components/schemas/Int32Type"},
"gateway_cluster_discovery_resolve_failures_total": {"$ref": "#/components/schemas/Int32Type"},
"gateway_cluster_membership_transitions_total": {
"type": "object",
"properties": {
"up": {"$ref": "#/components/schemas/Int32Type"},
"down": {"$ref": "#/components/schemas/Int32Type"}
},
"required": ["up", "down"],
"additionalProperties": false
},
"gateway_node_router_owner_resolutions_total": {
"type": "object",
"properties": {
"self": {"$ref": "#/components/schemas/Int32Type"},
"peer": {"$ref": "#/components/schemas/Int32Type"}
},
"required": ["self", "peer"],
"additionalProperties": false
}
},
"required": [
"gateway_cluster_member_count",
"gateway_cluster_discovery_resolve_failures_total",
"gateway_cluster_membership_transitions_total",
"gateway_node_router_owner_resolutions_total"
],
"additionalProperties": false
}
},
"required": [
"node_id",
@@ -1355,6 +1355,7 @@ export class MessageSendService {
attachments: attachmentsToProcess,
attachmentUploadUserId: user.id,
processedAttachments: favoriteMemeAttachment ? [favoriteMemeAttachment] : undefined,
stickerIds: data.sticker_ids ? data.sticker_ids.flatMap((stickerId) => createStickerID(stickerId)) : undefined,
messageReference,
messageSnapshots,
guildId: null,
@@ -2,8 +2,8 @@
import type {WebhookEvent} from 'livekit-server-sdk';
import {describe, expect, it, vi} from 'vitest';
import type {ChannelID, GuildID} from '../../BrandedTypes';
import {createChannelID, createGuildID} from '../../BrandedTypes';
import type {IVoiceRepository} from '../../voice/IVoiceRepository';
import {VoiceTopology} from '../../voice/VoiceTopology';
import type {IGatewayService} from '../IGatewayService';
import type {ILiveKitService} from '../ILiveKitService';
@@ -29,7 +29,12 @@ function harness(pinnedServerId: string | null) {
} as unknown as IVoiceRoomStore;
const gatewayService = {disconnectAllVoiceUsersInChannel} as unknown as IGatewayService;
const liveKitService = {} as unknown as ILiveKitService;
const service = new LiveKitWebhookService(voiceRoomStore, gatewayService, liveKitService, new VoiceTopology());
const service = new LiveKitWebhookService(
voiceRoomStore,
gatewayService,
liveKitService,
new VoiceTopology({} as unknown as IVoiceRepository, null),
);
return {service, deleteRoomServer, disconnectAllVoiceUsersInChannel};
}
@@ -1140,6 +1140,20 @@ export const NodeStatsResponse = z.object({
process_count: Int32Type,
process_limit: Int32Type,
uptime_seconds: Int32Type,
cluster_metrics: z
.object({
gateway_cluster_member_count: Int32Type,
gateway_cluster_discovery_resolve_failures_total: Int32Type,
gateway_cluster_membership_transitions_total: z.object({
up: Int32Type,
down: Int32Type,
}),
gateway_node_router_owner_resolutions_total: z.object({
self: Int32Type,
peer: Int32Type,
}),
})
.optional(),
}),
)
.max(1000),
+57 -16
View File
@@ -446,27 +446,39 @@ fn run_promote(args: PromoteArgs) -> Result<()> {
return Ok(());
}
for other in COMPONENTS
.iter()
.filter(|other| other.image != component.image)
{
for tag in &tags {
inspect_digest(&format!("{}/{}:{tag}", args.registry, other.image)).with_context(
|| {
format!(
"Refusing to advance {tag} for {}: {} has no published {tag} image; run the build-{} workflow first",
component.image,
other.image,
workflow_suffix(other.image)
)
},
)?;
}
for warning in unpublished_moving_tags(&args.registry, component.image, &tags, |reference| {
inspect_digest(reference).is_ok()
}) {
println!("warning: {warning}");
}
run_command(promote_command(&image, &digest, &tags))
}
fn unpublished_moving_tags<F>(
registry: &str,
component: &str,
tags: &[String],
mut published: F,
) -> Vec<String>
where
F: FnMut(&str) -> bool,
{
let mut warnings = Vec::new();
for other in COMPONENTS.iter().filter(|other| other.image != component) {
for tag in tags {
if !published(&format!("{registry}/{}:{tag}", other.image)) {
warnings.push(format!(
"{} has no published {tag} image, so {tag} stays an incomplete set until the build-{} workflow runs",
other.image,
workflow_suffix(other.image)
));
}
}
}
warnings
}
fn write_release_files(out_dir: &Path, manifest: &ImageSetManifest) -> Result<()> {
fs::create_dir_all(out_dir)
.with_context(|| format!("Failed to create {}", out_dir.display()))?;
@@ -1097,6 +1109,35 @@ mod tests {
);
}
#[test]
fn unpublished_moving_tags_are_reported_without_blocking_the_promote() {
let tags = ["v1".to_string(), "latest".to_string()];
let complete = unpublished_moving_tags("ghcr.io/fluxerapp", "fluxer-docs", &tags, |_| true);
assert!(complete.is_empty());
let bootstrapping =
unpublished_moving_tags("ghcr.io/fluxerapp", "fluxer-docs", &tags, |reference| {
!reference.starts_with("ghcr.io/fluxerapp/fluxer-recon:")
});
assert_eq!(
bootstrapping,
[
"fluxer-recon has no published v1 image, so v1 stays an incomplete set until the build-recon workflow runs",
"fluxer-recon has no published latest image, so latest stays an incomplete set until the build-recon workflow runs",
]
);
}
#[test]
fn unpublished_moving_tags_never_reports_the_component_being_promoted() {
let tags = ["v1".to_string()];
let warnings =
unpublished_moving_tags("ghcr.io/fluxerapp", "fluxer-recon", &tags, |reference| {
!reference.starts_with("ghcr.io/fluxerapp/fluxer-recon:")
});
assert!(warnings.is_empty());
}
#[test]
fn calver_tag_for_digest_picks_the_single_calver_tag() {
let tags = ["2026.630.20736", "latest", "v1"].map(str::to_string);