anodizer_core/config/archives.rs
1use std::collections::HashMap;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Deserializer, Serialize};
5
6use super::{
7 ArchiveHooksConfig, SignConfig, StringOrBool, StringOrU32, deserialize_string_or_bool_opt,
8};
9
10// ---------------------------------------------------------------------------
11// ArchivesConfig — untagged enum: false => Disabled, array => Configs
12// ---------------------------------------------------------------------------
13
14#[derive(Debug, Clone, JsonSchema)]
15pub enum ArchivesConfig {
16 Disabled,
17 Configs(Vec<ArchiveConfig>),
18}
19
20impl Serialize for ArchivesConfig {
21 fn serialize<S: serde::Serializer>(
22 &self,
23 serializer: S,
24 ) -> std::result::Result<S::Ok, S::Error> {
25 match self {
26 ArchivesConfig::Disabled => serializer.serialize_bool(false),
27 ArchivesConfig::Configs(configs) => configs.serialize(serializer),
28 }
29 }
30}
31
32impl Default for ArchivesConfig {
33 fn default() -> Self {
34 ArchivesConfig::Configs(vec![])
35 }
36}
37
38/// Custom deserializer for ArchivesConfig.
39/// Accepts:
40/// - boolean `false` → Disabled
41/// - array → Configs(...)
42/// - missing/null → Configs([]) (via serde default)
43pub(super) fn deserialize_archives_config<'de, D>(
44 deserializer: D,
45) -> Result<ArchivesConfig, D::Error>
46where
47 D: Deserializer<'de>,
48{
49 use serde::de::{self, Visitor};
50
51 struct ArchivesVisitor;
52
53 impl<'de> Visitor<'de> for ArchivesVisitor {
54 type Value = ArchivesConfig;
55
56 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.write_str("false or a list of archive configs")
58 }
59
60 fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
61 if !v {
62 Ok(ArchivesConfig::Disabled)
63 } else {
64 Err(E::custom(
65 "archives: true is not valid; use false or a list",
66 ))
67 }
68 }
69
70 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
71 let mut configs = Vec::new();
72 while let Some(item) = seq.next_element::<ArchiveConfig>()? {
73 configs.push(item);
74 }
75 Ok(ArchivesConfig::Configs(configs))
76 }
77
78 // Handle YAML null / missing when serde calls the deserializer explicitly.
79 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
80 Ok(ArchivesConfig::Configs(vec![]))
81 }
82
83 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
84 Ok(ArchivesConfig::Configs(vec![]))
85 }
86 }
87
88 deserializer.deserialize_any(ArchivesVisitor)
89}
90
91/// Custom deserializer for the `signs` / `sign` field.
92/// Accepts:
93/// - null/missing → empty vec (via serde default)
94/// - a single object → vec of one SignConfig
95/// - an array → vec of SignConfig
96pub(super) fn deserialize_signs<'de, D>(deserializer: D) -> Result<Vec<SignConfig>, D::Error>
97where
98 D: Deserializer<'de>,
99{
100 use serde::de::{self, Visitor};
101
102 struct SignsVisitor;
103
104 impl<'de> Visitor<'de> for SignsVisitor {
105 type Value = Vec<SignConfig>;
106
107 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 f.write_str("a sign config object or an array of sign config objects")
109 }
110
111 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
112 let mut configs = Vec::new();
113 while let Some(item) = seq.next_element::<SignConfig>()? {
114 configs.push(item);
115 }
116 Ok(configs)
117 }
118
119 fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
120 let config = SignConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
121 Ok(vec![config])
122 }
123
124 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
125 Ok(Vec::new())
126 }
127
128 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
129 Ok(Vec::new())
130 }
131 }
132
133 deserializer.deserialize_any(SignsVisitor)
134}
135
136// `binary_signs[].artifacts` is constrained at deserialize time (not as a
137// serde-typed enum) because `SignConfig` is shared with the top-level `signs:`
138// field, which legitimately accepts a wider set (`all`, `archive`, `binary`,
139// `checksum`, `package`, `sbom`, `none`). Promoting `artifacts` to an enum
140// would either narrow that surface or require a parallel `BinarySignConfig`
141// type duplicating every `SignConfig` field — the runtime check below keeps
142// `SignConfig` a single shared shape while still rejecting misconfigured
143// `binary_signs` entries at config-load time.
144//
145// The JSON schema for `binary_signs[]` therefore inherits `SignConfig`'s
146// unconstrained `artifacts: Option<String>` — the constraint lives in the
147// custom deserializer below and is exercised by the parse-time tests
148// `test_binary_signs_artifacts_*` further down this file.
149
150/// Wraps [`deserialize_signs`] and enforces that each entry's `artifacts`
151/// is one of the binary-only allowed values (`binary`, `none`, or omitted).
152/// Catches misconfiguration at load time instead of producing a silent
153/// no-op signing pipe.
154pub(super) fn deserialize_binary_signs<'de, D>(deserializer: D) -> Result<Vec<SignConfig>, D::Error>
155where
156 D: Deserializer<'de>,
157{
158 let configs = deserialize_signs(deserializer)?;
159 for (idx, cfg) in configs.iter().enumerate() {
160 if let Some(art) = cfg.artifacts.as_deref()
161 && art != "binary"
162 && art != "none"
163 {
164 return Err(serde::de::Error::custom(format!(
165 "binary_signs[{idx}].artifacts: '{art}' is not allowed; \
166 binary_signs accepts only 'binary' or 'none' (use top-level \
167 `signs:` for broader artifact filters)"
168 )));
169 }
170 }
171 Ok(configs)
172}
173
174// ---------------------------------------------------------------------------
175// WrapInDirectory – accepts bool (true = default dir name) or string
176// ---------------------------------------------------------------------------
177
178#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
179#[serde(untagged)]
180pub enum WrapInDirectory {
181 Bool(bool),
182 Name(String),
183}
184
185impl<'de> serde::Deserialize<'de> for WrapInDirectory {
186 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
187 let value = serde_yaml_ng::Value::deserialize(deserializer)?;
188 match value {
189 serde_yaml_ng::Value::Bool(b) => Ok(WrapInDirectory::Bool(b)),
190 serde_yaml_ng::Value::String(s) => Ok(WrapInDirectory::Name(s)),
191 _ => Err(serde::de::Error::custom("expected bool or string")),
192 }
193 }
194}
195
196impl WrapInDirectory {
197 /// Resolve the directory name to wrap archive contents in.
198 ///
199 /// When `true`, uses `default_name` (typically the archive stem).
200 /// When `false` or an empty string, returns `None` (no wrapping).
201 /// Otherwise returns the custom name.
202 pub fn directory_name(&self, default_name: &str) -> Option<String> {
203 match self {
204 WrapInDirectory::Bool(true) => Some(default_name.to_string()),
205 WrapInDirectory::Bool(false) => None,
206 WrapInDirectory::Name(s) if s.is_empty() => None,
207 WrapInDirectory::Name(s) => Some(s.clone()),
208 }
209 }
210}
211
212// ---------------------------------------------------------------------------
213// ArchiveConfig
214// ---------------------------------------------------------------------------
215
216#[derive(Debug, Clone, Serialize, Default, JsonSchema)]
217pub struct ArchiveConfig {
218 /// Unique identifier for cross-referencing this archive from other configs.
219 /// Defaults to `"default"` so a parse->serialise->reparse round-trip is
220 /// stable (GoReleaser stores this verbatim, not as an Option).
221 pub id: Option<String>,
222 /// Archive filename template (supports templates, e.g., "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}").
223 pub name_template: Option<String>,
224 /// Archive formats: tar.gz, tar.xz, tar.zst, tar, zip, gz, xz, or binary.
225 /// `gz` and `xz` are single-file compressors — supplying multiple input
226 /// files errors. Plural list; one archive per format is produced for each
227 /// target.
228 pub formats: Option<Vec<String>>,
229 /// Per-OS format overrides for this archive config.
230 pub format_overrides: Option<Vec<FormatOverride>>,
231 /// Extra files to include in the archive (glob patterns or detailed src/dst specs).
232 pub files: Option<Vec<ArchiveFileSpec>>,
233 /// Binary names to include (defaults to all binaries from matched builds).
234 pub binaries: Option<Vec<String>>,
235 /// When set, wrap archive contents in a top-level directory.
236 /// Accepts `true` (use archive stem as directory name), `false` (no wrapping),
237 /// or a string template for a custom directory name.
238 pub wrap_in_directory: Option<WrapInDirectory>,
239 /// Build IDs filter: only include artifacts from builds whose `id` is in this list.
240 pub ids: Option<Vec<String>>,
241 /// When true, create archive with no binaries (metadata-only).
242 pub meta: Option<bool>,
243 /// File permissions applied to binaries in archives.
244 pub builds_info: Option<ArchiveFileInfo>,
245 /// Strip binary parent directory in archive (place binaries at archive root).
246 pub strip_binary_directory: Option<bool>,
247 /// Allow different binary counts across targets. Default false (warn on mismatch).
248 pub allow_different_binary_count: Option<bool>,
249 /// Pre/post archive hooks (`before`/`after`).
250 pub hooks: Option<ArchiveHooksConfig>,
251}
252
253/// Fold a deprecated singular `format: tar.gz` into the canonical
254/// `formats: [tar.gz]` list, emitting a `tracing::warn!` deprecation notice
255/// keyed by `context_label` (the archive id or override `os=` so the user
256/// can locate the offending entry). Returns the folded list (creating one
257/// if `formats` was `None` and `legacy` is `Some`).
258///
259/// Shared by `ArchiveConfig` and `FormatOverride` to keep the deprecation
260/// message + fold semantics in one place.
261fn fold_format_into_formats(
262 context_label: &str,
263 context_kind: &str,
264 formats: Option<Vec<String>>,
265 legacy: Option<String>,
266) -> Option<Vec<String>> {
267 let mut formats = formats;
268 if let Some(legacy) = legacy {
269 tracing::warn!(
270 "DEPRECATION: {}[{}]: 'format: {}' is deprecated; \
271 use 'formats: [{}]' instead.",
272 context_kind,
273 context_label,
274 legacy,
275 legacy
276 );
277 formats.get_or_insert_with(Vec::new).push(legacy);
278 }
279 formats
280}
281
282// Custom Deserialize that accepts deprecated GR aliases:
283// - `format: tar.gz` (singular String) folded into `formats: [tar.gz]`
284// (`internal/pipe/archive/archive.go:62-64`)
285// - `builds: [foo]` folded into `ids: [foo]`
286// (`internal/pipe/archive/archive.go:79-82`)
287// Each alias hit emits a `tracing::warn!` deprecation notice.
288impl<'de> Deserialize<'de> for ArchiveConfig {
289 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
290 where
291 D: Deserializer<'de>,
292 {
293 #[derive(Deserialize, Default)]
294 #[serde(default)]
295 struct Raw {
296 id: Option<String>,
297 name_template: Option<String>,
298 formats: Option<Vec<String>>,
299 format: Option<String>,
300 format_overrides: Option<Vec<FormatOverride>>,
301 files: Option<Vec<ArchiveFileSpec>>,
302 binaries: Option<Vec<String>>,
303 wrap_in_directory: Option<WrapInDirectory>,
304 ids: Option<Vec<String>>,
305 builds: Option<Vec<String>>,
306 meta: Option<bool>,
307 builds_info: Option<ArchiveFileInfo>,
308 strip_binary_directory: Option<bool>,
309 allow_different_binary_count: Option<bool>,
310 hooks: Option<ArchiveHooksConfig>,
311 }
312
313 let raw = Raw::deserialize(deserializer)?;
314
315 let id_label = raw.id.clone().unwrap_or_else(|| "default".to_string());
316 let formats = fold_format_into_formats(
317 &format!("id={}", id_label),
318 "archives",
319 raw.formats,
320 raw.format,
321 );
322 let mut ids = raw.ids;
323 if let Some(legacy) = raw.builds {
324 tracing::warn!(
325 "DEPRECATION: archives[id={}]: 'builds: {:?}' is deprecated; \
326 use 'ids: [...]' instead.",
327 id_label,
328 legacy
329 );
330 let target = ids.get_or_insert_with(Vec::new);
331 target.extend(legacy);
332 }
333
334 Ok(ArchiveConfig {
335 id: raw.id.or_else(|| Some("default".to_string())),
336 name_template: raw.name_template,
337 formats,
338 format_overrides: raw.format_overrides,
339 files: raw.files,
340 binaries: raw.binaries,
341 wrap_in_directory: raw.wrap_in_directory,
342 ids,
343 meta: raw.meta,
344 builds_info: raw.builds_info,
345 strip_binary_directory: raw.strip_binary_directory,
346 allow_different_binary_count: raw.allow_different_binary_count,
347 hooks: raw.hooks,
348 })
349 }
350}
351
352#[derive(Debug, Clone, Serialize, JsonSchema)]
353pub struct FormatOverride {
354 /// Operating system this override applies to (e.g., "windows", "darwin", "linux").
355 pub os: String,
356 /// Plural format overrides for this OS: tar.gz, tar.xz, tar.zst, tar, zip,
357 /// gz, xz, or binary.
358 pub formats: Option<Vec<String>>,
359}
360
361// Custom Deserialize that accepts both `formats: [tar.gz]` (canonical) and
362// the deprecated singular `format: tar.gz` (GR
363// `internal/pipe/archive/archive.go:71-74`). The legacy spelling is folded
364// into `formats` at parse time via the shared `fold_format_into_formats`
365// helper, which also emits the deprecation warning.
366impl<'de> Deserialize<'de> for FormatOverride {
367 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
368 where
369 D: Deserializer<'de>,
370 {
371 #[derive(Deserialize, Default)]
372 #[serde(default)]
373 struct Raw {
374 os: String,
375 formats: Option<Vec<String>>,
376 format: Option<String>,
377 }
378 let raw = Raw::deserialize(deserializer)?;
379 let formats = fold_format_into_formats(
380 &format!("os={}", raw.os),
381 "archives.format_overrides",
382 raw.formats,
383 raw.format,
384 );
385 Ok(FormatOverride {
386 os: raw.os,
387 formats,
388 })
389 }
390}
391
392/// Specifies a file to include in archives. Can be a simple glob string or a
393/// detailed object with src/dst/info fields for controlling archive placement
394/// and file metadata.
395///
396/// NOTE: This is intentionally a separate type from [`ExtraFileSpec`] (used for
397/// checksum/release extra_files). `ArchiveFileSpec` needs `src`/`dst`/`info`
398/// fields for archive placement and file metadata (owner, group, mode, mtime),
399/// while `ExtraFileSpec` needs `glob`/`name_template` for checksumming and
400/// upload renaming. The fields and semantics are different enough that a unified
401/// type would be confusing.
402#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
403#[serde(untagged)]
404pub enum ArchiveFileSpec {
405 Glob(String),
406 Detailed {
407 src: String,
408 dst: Option<String>,
409 info: Option<ArchiveFileInfo>,
410 /// When true, strip the parent directory from the file path in the archive.
411 strip_parent: Option<bool>,
412 },
413}
414
415impl PartialEq<&str> for ArchiveFileSpec {
416 fn eq(&self, other: &&str) -> bool {
417 match self {
418 ArchiveFileSpec::Glob(s) => s.as_str() == *other,
419 _ => false,
420 }
421 }
422}
423
424/// Shared file metadata (owner, group, mode, mtime) used by both archive entries
425/// and nFPM package contents. Previously duplicated as `ArchiveFileInfo` and
426/// `NfpmFileInfo`; now unified.
427#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
428#[serde(default)]
429pub struct FileInfo {
430 /// File owner name (e.g., "root").
431 pub owner: Option<String>,
432 /// File group name (e.g., "root").
433 pub group: Option<String>,
434 /// File permission mode. Accepts a YAML int (decimal, e.g. `420` for
435 /// `0o644`) or an octal-prefixed string (`"0o644"`, `"0644"`). This
436 /// matches GoReleaser's `uint32` type for `Mode` on archive/nfpm contents
437 /// while letting users spell octal naturally in YAML.
438 pub mode: Option<StringOrU32>,
439 /// File modification time in RFC3339 format (e.g., "2024-01-01T00:00:00Z").
440 pub mtime: Option<String>,
441}
442
443/// Backward-compatible alias for archive code.
444pub type ArchiveFileInfo = FileInfo;
445
446/// Parse an octal mode string into a `u32`, handling common YAML-friendly
447/// representations: `"0755"`, `"0o755"`, `"0O755"`, `"755"`, and `"0"`.
448pub fn parse_octal_mode(s: &str) -> Option<u32> {
449 let cleaned = s
450 .strip_prefix("0o")
451 .or_else(|| s.strip_prefix("0O"))
452 .unwrap_or(s);
453 let cleaned = if cleaned.is_empty() { "0" } else { cleaned };
454 u32::from_str_radix(cleaned, 8).ok()
455}
456
457/// The set of archive format strings recognised by the archive stage.
458/// Used for early validation so typos are caught at config load time rather
459/// than mid-pipeline.
460pub const VALID_ARCHIVE_FORMATS: &[&str] = &[
461 "tar.gz", "tgz", "tar.xz", "txz", "tar.zst", "tzst", "tar", "zip", "gz", "xz", "binary", "none",
462];
463
464// ---------------------------------------------------------------------------
465// ChecksumConfig
466// ---------------------------------------------------------------------------
467
468/// Specifies an extra file to include in checksums or release uploads. Can be a
469/// simple glob string or a detailed object with glob and name_template fields.
470///
471/// See [`ArchiveFileSpec`] doc comment for why this is a separate type.
472#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
473#[serde(untagged)]
474pub enum ExtraFileSpec {
475 Glob(String),
476 Detailed {
477 glob: String,
478 /// Optional override for the upload filename.
479 #[serde(default)]
480 name_template: Option<String>,
481 /// When true, treat a glob that matches zero files as a no-op
482 /// rather than a hard error. Useful for assets produced only in
483 /// CI (e.g. signing public keys derived from a secret) that
484 /// must not break local snapshot/dry-run flows. Defaults to
485 /// false, matching the prior fail-fast behavior.
486 #[serde(default)]
487 allow_empty: bool,
488 },
489}
490
491impl ExtraFileSpec {
492 /// Return the glob pattern for this spec.
493 pub fn glob(&self) -> &str {
494 match self {
495 ExtraFileSpec::Glob(s) => s,
496 ExtraFileSpec::Detailed { glob, .. } => glob,
497 }
498 }
499
500 /// Return the optional name_template (only present in Detailed variant).
501 pub fn name_template(&self) -> Option<&str> {
502 match self {
503 ExtraFileSpec::Glob(_) => None,
504 ExtraFileSpec::Detailed { name_template, .. } => name_template.as_deref(),
505 }
506 }
507
508 /// Return whether this spec allows a zero-match glob without erroring
509 /// (Detailed variant only; the bare string form is always fail-fast).
510 pub fn allow_empty(&self) -> bool {
511 match self {
512 ExtraFileSpec::Glob(_) => false,
513 ExtraFileSpec::Detailed { allow_empty, .. } => *allow_empty,
514 }
515 }
516}
517
518/// A file whose contents are rendered through the template engine before use.
519/// Used by `templated_extra_files` across multiple stages (GoReleaser Pro feature).
520#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
521#[serde(default)]
522pub struct TemplatedExtraFile {
523 /// Source template file path.
524 pub src: String,
525 /// Destination filename for the rendered output.
526 /// Supports template variables (e.g. `"{{ .ProjectName }}-NOTES.txt"`).
527 pub dst: Option<String>,
528 /// File permissions in octal notation as a string, e.g. `"0755"`.
529 /// Parsed at runtime via `parse_octal_mode()` to avoid YAML interpreting as decimal.
530 pub mode: Option<String>,
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
534#[serde(default)]
535pub struct ChecksumConfig {
536 /// Checksum filename template (default: "{{ .ProjectName }}_{{ .Version }}_checksums.txt").
537 pub name_template: Option<String>,
538 /// Hash algorithm: sha256, sha512, sha1, md5, crc32 (default: sha256).
539 pub algorithm: Option<String>,
540 /// Disable checksums. Accepts bool or template string.
541 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
542 pub skip: Option<StringOrBool>,
543 /// Extra files to include in the checksum file (beyond build artifacts).
544 pub extra_files: Option<Vec<ExtraFileSpec>>,
545 /// Extra files whose contents are rendered through the template engine before inclusion.
546 /// Unlike `extra_files` which copy as-is, template variables like `{{ .Tag }}` are expanded.
547 /// GoReleaser Pro feature.
548 pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
549 /// Build IDs filter: only checksum artifacts from builds whose `id` is in this list.
550 pub ids: Option<Vec<String>>,
551 /// When true, produce one checksum file per artifact instead of a combined file.
552 pub split: Option<bool>,
553}
554
555impl ChecksumConfig {
556 /// Default checksum filename template (combined mode). Mirrors
557 /// `internal/pipe/checksums/checksums.go:48` in GoReleaser.
558 pub const DEFAULT_NAME_TEMPLATE: &'static str = "{{ ProjectName }}_{{ Version }}_checksums.txt";
559
560 /// Default hash algorithm. Mirrors GoReleaser
561 /// (`internal/pipe/checksums/checksums.go:42`).
562 pub const DEFAULT_ALGORITHM: &'static str = "sha256";
563
564 /// Resolve the hash algorithm, falling back to the project default
565 /// when the user did not specify one. Stages MUST call this rather
566 /// than reading `self.algorithm` directly, so a future default change
567 /// (or user-facing override resolution) lands in one place.
568 pub fn resolved_algorithm(&self) -> &str {
569 self.algorithm.as_deref().unwrap_or(Self::DEFAULT_ALGORITHM)
570 }
571
572 /// Whether split-mode (one sidecar per artifact) is requested.
573 /// Defaults to `false` (combined-file mode, matching GoReleaser).
574 pub fn resolved_split(&self) -> bool {
575 self.split.unwrap_or(false)
576 }
577
578 /// Resolve the combined-mode checksum filename template, falling back
579 /// to the GoReleaser-canonical default. Returns the raw template
580 /// string; the caller still renders it through Tera.
581 ///
582 /// Split mode constructs sidecar names per-artifact at the call site
583 /// (`<artifact>.<algo>` literal format) and intentionally does NOT
584 /// route through this accessor — that path needs no template rendering.
585 pub fn resolved_combined_name_template(&self) -> &str {
586 self.name_template
587 .as_deref()
588 .unwrap_or(Self::DEFAULT_NAME_TEMPLATE)
589 }
590}
591
592// ---------------------------------------------------------------------------
593// ContentSource — inline string, from_file, or from_url
594// ---------------------------------------------------------------------------
595
596/// A content source that can be an inline string, read from a file, or fetched
597/// from a URL. Used for release header/footer values.
598///
599/// YAML examples:
600/// header: "inline text"
601/// header:
602/// from_file: ./RELEASE_HEADER.md
603/// header:
604/// from_url: https://example.com/header.md
605/// header:
606/// from_url: https://example.com/header.md
607/// headers:
608/// X-API-Token: "{{ .Env.API_TOKEN }}"
609/// Accept: "text/markdown"
610///
611/// Both `from_file` path and `from_url` URL are template-rendered before use.
612/// Header values are template-rendered. (GoReleaser Pro parity.)
613#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
614#[serde(untagged)]
615pub enum ContentSource {
616 Inline(String),
617 FromFile {
618 from_file: String,
619 },
620 FromUrl {
621 from_url: String,
622 /// Optional HTTP headers (value templates allowed). Enables private
623 /// mirrors and authenticated endpoints.
624 #[serde(default, skip_serializing_if = "Option::is_none")]
625 headers: Option<HashMap<String, String>>,
626 },
627}
628
629impl PartialEq for ContentSource {
630 fn eq(&self, other: &Self) -> bool {
631 match (self, other) {
632 (Self::Inline(a), Self::Inline(b)) => a == b,
633 (Self::FromFile { from_file: a }, Self::FromFile { from_file: b }) => a == b,
634 (
635 Self::FromUrl {
636 from_url: a,
637 headers: ha,
638 },
639 Self::FromUrl {
640 from_url: b,
641 headers: hb,
642 },
643 ) => a == b && ha == hb,
644 _ => false,
645 }
646 }
647}