1use std::{
4 borrow::Cow,
5 fmt::{self, Display},
6};
7
8use indexmap::IndexMap;
9use self_cell::self_cell;
10use serde::{Deserialize, Deserializer, Serialize, de};
11
12pub mod expr;
13
14#[derive(Deserialize, Debug, PartialEq)]
16#[serde(rename_all = "kebab-case", untagged)]
17pub enum Permissions {
18 Base(BasePermission),
20 Explicit(IndexMap<String, Permission>),
25}
26
27impl Default for Permissions {
28 fn default() -> Self {
29 Self::Base(BasePermission::Default)
30 }
31}
32
33#[derive(Deserialize, Debug, Default, PartialEq)]
36#[serde(rename_all = "kebab-case")]
37pub enum BasePermission {
38 #[default]
40 Default,
41 ReadAll,
43 WriteAll,
45}
46
47#[derive(Deserialize, Debug, Default, PartialEq)]
49#[serde(rename_all = "kebab-case")]
50pub enum Permission {
51 Read,
53
54 Write,
56
57 #[default]
59 None,
60}
61
62pub type Env = IndexMap<String, EnvValue>;
64
65#[derive(Deserialize, Serialize, Debug, PartialEq)]
73#[serde(untagged)]
74pub enum EnvValue {
75 #[serde(deserialize_with = "null_to_default")]
77 String(String),
78 Number(f64),
79 Boolean(bool),
80}
81
82impl Display for EnvValue {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 match self {
85 Self::String(s) => write!(f, "{s}"),
86 Self::Number(n) => write!(f, "{n}"),
87 Self::Boolean(b) => write!(f, "{b}"),
88 }
89 }
90}
91
92impl EnvValue {
93 pub fn is_empty(&self) -> bool {
97 match self {
98 EnvValue::String(s) => s.is_empty(),
99 _ => false,
100 }
101 }
102
103 pub fn csharp_bool(&self) -> bool {
110 match self {
111 EnvValue::Boolean(true) => true,
112 EnvValue::String(maybe) => maybe.trim().eq_ignore_ascii_case("true"),
113 _ => false,
114 }
115 }
116
117 pub fn actions_toolkit_bool(&self) -> Option<bool> {
124 match self {
125 EnvValue::Boolean(b) => Some(*b),
126 EnvValue::String(s) if matches!(s.trim(), "true" | "True" | "TRUE") => Some(true),
127 EnvValue::String(s) if matches!(s.trim(), "false" | "False" | "FALSE") => Some(false),
128 _ => None,
129 }
130 }
131}
132
133#[derive(Deserialize, Debug, PartialEq)]
138#[serde(untagged)]
139enum SoV<T> {
140 One(T),
141 Many(Vec<T>),
142}
143
144impl<T> From<SoV<T>> for Vec<T> {
145 fn from(val: SoV<T>) -> Vec<T> {
146 match val {
147 SoV::One(v) => vec![v],
148 SoV::Many(vs) => vs,
149 }
150 }
151}
152
153pub(crate) fn scalar_or_vector<'de, D, T>(de: D) -> Result<Vec<T>, D::Error>
154where
155 D: Deserializer<'de>,
156 T: Deserialize<'de>,
157{
158 SoV::deserialize(de).map(Into::into)
159}
160
161#[derive(Deserialize, Debug, PartialEq)]
165#[serde(untagged)]
166enum BoS {
167 Bool(bool),
168 String(String),
169}
170
171impl From<BoS> for String {
172 fn from(value: BoS) -> Self {
173 match value {
174 BoS::Bool(b) => b.to_string(),
175 BoS::String(s) => s,
176 }
177 }
178}
179
180#[derive(Serialize, Debug, PartialEq)]
189pub enum If {
190 Bool(bool),
191 Expr(String),
194}
195
196impl<'de> Deserialize<'de> for If {
197 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
198 where
199 D: Deserializer<'de>,
200 {
201 #[derive(Deserialize)]
204 #[serde(untagged)]
205 enum RawIf {
206 Bool(bool),
207 Int(i64),
208 Float(f64),
209 Expr(String),
210 }
211
212 match RawIf::deserialize(deserializer)? {
213 RawIf::Bool(b) => Ok(If::Bool(b)),
214 RawIf::Int(n) => Ok(If::Bool(n != 0)),
215 RawIf::Float(f) => Ok(If::Bool(f != 0.0 && !f.is_nan())),
216 RawIf::Expr(s) => Ok(If::Expr(s)),
217 }
218 }
219}
220
221pub(crate) fn bool_is_string<'de, D>(de: D) -> Result<String, D::Error>
222where
223 D: Deserializer<'de>,
224{
225 BoS::deserialize(de).map(Into::into)
226}
227
228fn null_to_default<'de, D, T>(de: D) -> Result<T, D::Error>
229where
230 D: Deserializer<'de>,
231 T: Default + Deserialize<'de>,
232{
233 let key = Option::<T>::deserialize(de)?;
234 Ok(key.unwrap_or_default())
235}
236
237#[derive(Debug, PartialEq)]
239pub struct UsesError(String);
240
241impl fmt::Display for UsesError {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 write!(f, "malformed `uses` ref: {}", self.0)
244 }
245}
246
247#[derive(Debug, PartialEq)]
248pub enum Uses {
249 Local(LocalUses),
251
252 Repository(RepositoryUses),
254
255 Docker(DockerUses),
257}
258
259impl Uses {
260 pub fn parse<'a>(uses: impl Into<Cow<'a, str>>) -> Result<Self, UsesError> {
262 let uses = uses.into();
263 let uses = uses.trim();
264
265 if uses.starts_with("./") {
266 Ok(Self::Local(LocalUses::new(uses)))
267 } else if let Some(image) = uses.strip_prefix("docker://") {
268 Ok(Self::Docker(DockerUses::parse(image)))
269 } else {
270 RepositoryUses::parse(uses).map(Self::Repository)
271 }
272 }
273
274 pub fn raw(&self) -> &str {
276 match self {
277 Uses::Local(local) => &local.path,
278 Uses::Repository(repo) => repo.raw(),
279 Uses::Docker(docker) => docker.raw(),
280 }
281 }
282}
283
284#[derive(Debug, PartialEq)]
286#[non_exhaustive]
287pub struct LocalUses {
288 pub path: String,
289}
290
291impl LocalUses {
292 fn new(path: impl Into<String>) -> Self {
293 LocalUses { path: path.into() }
294 }
295}
296
297#[derive(Debug, PartialEq)]
298struct RepositoryUsesInner<'a> {
299 owner: &'a str,
301 repo: &'a str,
303 slug: &'a str,
305 subpath: Option<&'a str>,
307 git_ref: &'a str,
309}
310
311impl<'a> RepositoryUsesInner<'a> {
312 fn from_str(uses: &'a str) -> Result<Self, UsesError> {
313 let uses = uses.trim();
315
316 let (path, git_ref) = match uses.rsplit_once('@') {
319 Some((path, git_ref)) => (path, git_ref),
320 None => return Err(UsesError(format!("missing `@<ref>` in {uses}"))),
321 };
322
323 let mut components = path.splitn(3, '/');
324
325 if let Some(owner) = components.next()
326 && let Some(repo) = components.next()
327 {
328 let subpath = components.next();
329
330 let slug = if subpath.is_none() {
331 path
332 } else {
333 &path[..owner.len() + 1 + repo.len()]
334 };
335
336 Ok(RepositoryUsesInner {
337 owner,
338 repo,
339 slug,
340 subpath,
341 git_ref,
342 })
343 } else {
344 Err(UsesError(format!("owner/repo slug is too short: {uses}")))
345 }
346 }
347}
348
349self_cell!(
350 pub struct RepositoryUses {
352 owner: String,
353
354 #[covariant]
355 dependent: RepositoryUsesInner,
356 }
357
358 impl {Debug, PartialEq}
359);
360
361impl Display for RepositoryUses {
362 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363 write!(f, "{}", self.raw())
364 }
365}
366
367impl RepositoryUses {
368 pub fn parse(uses: impl Into<String>) -> Result<Self, UsesError> {
370 RepositoryUses::try_new(uses.into(), |s| {
371 let inner = RepositoryUsesInner::from_str(s)?;
372 Ok(inner)
373 })
374 }
375
376 pub fn raw(&self) -> &str {
378 self.borrow_owner()
379 }
380
381 pub fn owner(&self) -> &str {
383 self.borrow_dependent().owner
384 }
385
386 pub fn repo(&self) -> &str {
388 self.borrow_dependent().repo
389 }
390
391 pub fn slug(&self) -> &str {
393 self.borrow_dependent().slug
394 }
395
396 pub fn subpath(&self) -> Option<&str> {
398 self.borrow_dependent().subpath
399 }
400
401 pub fn git_ref(&self) -> &str {
403 self.borrow_dependent().git_ref
404 }
405}
406
407#[derive(Debug, PartialEq)]
408#[non_exhaustive]
409pub struct DockerUsesInner<'a> {
410 registry: Option<&'a str>,
412 image: &'a str,
414 tag: Option<&'a str>,
416 hash: Option<&'a str>,
418}
419
420impl<'a> DockerUsesInner<'a> {
421 fn is_registry(registry: &str) -> bool {
422 registry == "localhost" || registry.contains('.') || registry.contains(':')
424 }
425
426 fn from_str(uses: &'a str) -> Self {
427 let uses = uses.trim();
429
430 let (registry, image) = match uses.split_once('/') {
431 Some((registry, image)) if Self::is_registry(registry) => (Some(registry), image),
432 _ => (None, uses),
433 };
434
435 if let Some(at_pos) = image.find('@') {
439 let (image, hash) = image.split_at(at_pos);
440
441 let hash = if hash.is_empty() {
442 None
443 } else {
444 Some(&hash[1..])
445 };
446
447 DockerUsesInner {
448 registry,
449 image,
450 tag: None,
451 hash,
452 }
453 } else {
454 let (image, tag) = match image.split_once(':') {
455 Some((image, "")) => (image, None),
456 Some((image, tag)) => (image, Some(tag)),
457 _ => (image, None),
458 };
459
460 DockerUsesInner {
461 registry,
462 image,
463 tag,
464 hash: None,
465 }
466 }
467 }
468}
469
470self_cell!(
471 pub struct DockerUses {
473 owner: String,
474
475 #[covariant]
476 dependent: DockerUsesInner,
477 }
478
479 impl {Debug, PartialEq}
480);
481
482impl DockerUses {
483 pub fn parse(uses: impl Into<String>) -> Self {
485 DockerUses::new(uses.into(), |s| DockerUsesInner::from_str(s))
486 }
487
488 pub fn raw(&self) -> &str {
490 self.borrow_owner()
491 }
492
493 pub fn registry(&self) -> Option<&str> {
495 self.borrow_dependent().registry
496 }
497
498 pub fn image(&self) -> &str {
500 self.borrow_dependent().image
501 }
502
503 pub fn tag(&self) -> Option<&str> {
505 self.borrow_dependent().tag
506 }
507
508 pub fn hash(&self) -> Option<&str> {
510 self.borrow_dependent().hash
511 }
512}
513
514impl<'de> Deserialize<'de> for DockerUses {
515 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
516 where
517 D: Deserializer<'de>,
518 {
519 let uses = <Cow<'de, str>>::deserialize(deserializer)?;
520 Ok(DockerUses::parse(uses))
521 }
522}
523
524pub(crate) fn custom_error<'de, D>(msg: impl Display) -> D::Error
530where
531 D: Deserializer<'de>,
532{
533 let msg = msg.to_string();
534 tracing::error!(msg);
535 de::Error::custom(msg)
536}
537
538pub(crate) fn step_uses<'de, D>(de: D) -> Result<Uses, D::Error>
540where
541 D: Deserializer<'de>,
542{
543 let uses = <Cow<'de, str>>::deserialize(de)?;
544 Uses::parse(uses).map_err(custom_error::<D>)
545}
546
547pub(crate) fn reusable_step_uses<'de, D>(de: D) -> Result<Uses, D::Error>
549where
550 D: Deserializer<'de>,
551{
552 let uses = step_uses(de)?;
553
554 match uses {
555 Uses::Repository(_) => Ok(uses),
556 Uses::Local(ref local) => {
557 if local.path.contains('@') {
562 Err(custom_error::<D>(
563 "local reusable workflow reference can't specify `@<ref>`",
564 ))
565 } else {
566 Ok(uses)
567 }
568 }
569 Uses::Docker(_) => Err(custom_error::<D>(
571 "docker action invalid in reusable workflow `uses`",
572 )),
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use indexmap::IndexMap;
579 use serde::Deserialize;
580
581 use crate::common::{BasePermission, Env, EnvValue, Permission};
582
583 use super::{Permissions, Uses, reusable_step_uses};
584
585 #[test]
586 fn test_permissions() {
587 assert_eq!(
588 yaml_serde::from_str::<Permissions>("read-all").unwrap(),
589 Permissions::Base(BasePermission::ReadAll)
590 );
591
592 let perm = "security-events: write";
593 assert_eq!(
594 yaml_serde::from_str::<Permissions>(perm).unwrap(),
595 Permissions::Explicit(IndexMap::from([(
596 "security-events".into(),
597 Permission::Write
598 )]))
599 );
600 }
601
602 #[test]
603 fn test_env_empty_value() {
604 let env = "foo:";
605 assert_eq!(
606 yaml_serde::from_str::<Env>(env).unwrap()["foo"],
607 EnvValue::String("".into())
608 );
609 }
610
611 #[test]
612 fn test_env_value_csharp_trueish() {
613 let vectors = [
614 (EnvValue::Boolean(true), true),
615 (EnvValue::Boolean(false), false),
616 (EnvValue::String("true".to_string()), true),
617 (EnvValue::String("TRUE".to_string()), true),
618 (EnvValue::String("TrUe".to_string()), true),
619 (EnvValue::String(" true ".to_string()), true),
620 (EnvValue::String(" \n\r\t True\n\n".to_string()), true),
621 (EnvValue::String("false".to_string()), false),
622 (EnvValue::String("1".to_string()), false),
623 (EnvValue::String("yes".to_string()), false),
624 (EnvValue::String("on".to_string()), false),
625 (EnvValue::String("random".to_string()), false),
626 (EnvValue::Number(1.0), false),
627 (EnvValue::Number(0.0), false),
628 (EnvValue::Number(666.0), false),
629 ];
630
631 for (val, expected) in vectors {
632 assert_eq!(val.csharp_bool(), expected, "failed for {val:?}");
633 }
634 }
635
636 #[test]
637 fn test_uses_parses() {
638 insta::assert_debug_snapshot!(
640 Uses::parse("actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
641 @r#"
642 Repository(
643 RepositoryUses {
644 owner: "actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
645 dependent: RepositoryUsesInner {
646 owner: "actions",
647 repo: "checkout",
648 slug: "actions/checkout",
649 subpath: None,
650 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
651 },
652 },
653 )
654 "#,
655 );
656
657 insta::assert_debug_snapshot!(
659 Uses::parse("actions/aws/ec2@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
660 @r#"
661 Repository(
662 RepositoryUses {
663 owner: "actions/aws/ec2@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
664 dependent: RepositoryUsesInner {
665 owner: "actions",
666 repo: "aws",
667 slug: "actions/aws",
668 subpath: Some(
669 "ec2",
670 ),
671 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
672 },
673 },
674 )
675 "#
676 );
677
678 insta::assert_debug_snapshot!(
680 Uses::parse("example/foo/bar/baz/quux@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
681 @r#"
682 Repository(
683 RepositoryUses {
684 owner: "example/foo/bar/baz/quux@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
685 dependent: RepositoryUsesInner {
686 owner: "example",
687 repo: "foo",
688 slug: "example/foo",
689 subpath: Some(
690 "bar/baz/quux",
691 ),
692 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
693 },
694 },
695 )
696 "#
697 );
698
699 insta::assert_debug_snapshot!(
701 Uses::parse("actions/checkout@v4").unwrap(),
702 @r#"
703 Repository(
704 RepositoryUses {
705 owner: "actions/checkout@v4",
706 dependent: RepositoryUsesInner {
707 owner: "actions",
708 repo: "checkout",
709 slug: "actions/checkout",
710 subpath: None,
711 git_ref: "v4",
712 },
713 },
714 )
715 "#
716 );
717
718 insta::assert_debug_snapshot!(
719 Uses::parse("actions/checkout@abcd").unwrap(),
720 @r#"
721 Repository(
722 RepositoryUses {
723 owner: "actions/checkout@abcd",
724 dependent: RepositoryUsesInner {
725 owner: "actions",
726 repo: "checkout",
727 slug: "actions/checkout",
728 subpath: None,
729 git_ref: "abcd",
730 },
731 },
732 )
733 "#
734 );
735
736 insta::assert_debug_snapshot!(
738 Uses::parse("actions/checkout").unwrap_err(),
739 @r#"
740 UsesError(
741 "missing `@<ref>` in actions/checkout",
742 )
743 "#
744 );
745
746 insta::assert_debug_snapshot!(
748 Uses::parse("docker://alpine:3.8").unwrap(),
749 @r#"
750 Docker(
751 DockerUses {
752 owner: "alpine:3.8",
753 dependent: DockerUsesInner {
754 registry: None,
755 image: "alpine",
756 tag: Some(
757 "3.8",
758 ),
759 hash: None,
760 },
761 },
762 )
763 "#
764 );
765
766 insta::assert_debug_snapshot!(
768 Uses::parse("docker://localhost/alpine:3.8").unwrap(),
769 @r#"
770 Docker(
771 DockerUses {
772 owner: "localhost/alpine:3.8",
773 dependent: DockerUsesInner {
774 registry: Some(
775 "localhost",
776 ),
777 image: "alpine",
778 tag: Some(
779 "3.8",
780 ),
781 hash: None,
782 },
783 },
784 )
785 "#
786 );
787
788 insta::assert_debug_snapshot!(
790 Uses::parse("docker://localhost:1337/alpine:3.8").unwrap(),
791 @r#"
792 Docker(
793 DockerUses {
794 owner: "localhost:1337/alpine:3.8",
795 dependent: DockerUsesInner {
796 registry: Some(
797 "localhost:1337",
798 ),
799 image: "alpine",
800 tag: Some(
801 "3.8",
802 ),
803 hash: None,
804 },
805 },
806 )
807 "#
808 );
809
810 insta::assert_debug_snapshot!(
812 Uses::parse("docker://ghcr.io/foo/alpine:3.8").unwrap(),
813 @r#"
814 Docker(
815 DockerUses {
816 owner: "ghcr.io/foo/alpine:3.8",
817 dependent: DockerUsesInner {
818 registry: Some(
819 "ghcr.io",
820 ),
821 image: "foo/alpine",
822 tag: Some(
823 "3.8",
824 ),
825 hash: None,
826 },
827 },
828 )
829 "#
830 );
831
832 insta::assert_debug_snapshot!(
834 Uses::parse("docker://ghcr.io/foo/alpine").unwrap(),
835 @r#"
836 Docker(
837 DockerUses {
838 owner: "ghcr.io/foo/alpine",
839 dependent: DockerUsesInner {
840 registry: Some(
841 "ghcr.io",
842 ),
843 image: "foo/alpine",
844 tag: None,
845 hash: None,
846 },
847 },
848 )
849 "#
850 );
851
852 insta::assert_debug_snapshot!(
854 Uses::parse("docker://ghcr.io/foo/alpine:").unwrap(),
855 @r#"
856 Docker(
857 DockerUses {
858 owner: "ghcr.io/foo/alpine:",
859 dependent: DockerUsesInner {
860 registry: Some(
861 "ghcr.io",
862 ),
863 image: "foo/alpine",
864 tag: None,
865 hash: None,
866 },
867 },
868 )
869 "#
870 );
871
872 insta::assert_debug_snapshot!(
874 Uses::parse("docker://alpine").unwrap(),
875 @r#"
876 Docker(
877 DockerUses {
878 owner: "alpine",
879 dependent: DockerUsesInner {
880 registry: None,
881 image: "alpine",
882 tag: None,
883 hash: None,
884 },
885 },
886 )
887 "#
888 );
889
890 insta::assert_debug_snapshot!(
892 Uses::parse("docker://alpine@hash").unwrap(),
893 @r#"
894 Docker(
895 DockerUses {
896 owner: "alpine@hash",
897 dependent: DockerUsesInner {
898 registry: None,
899 image: "alpine",
900 tag: None,
901 hash: Some(
902 "hash",
903 ),
904 },
905 },
906 )
907 "#
908 );
909
910 insta::assert_debug_snapshot!(
912 Uses::parse("./.github/actions/hello-world-action@172239021f7ba04fe7327647b213799853a9eb89").unwrap(),
913 @r#"
914 Local(
915 LocalUses {
916 path: "./.github/actions/hello-world-action@172239021f7ba04fe7327647b213799853a9eb89",
917 },
918 )
919 "#
920 );
921
922 insta::assert_debug_snapshot!(
924 Uses::parse("./.github/actions/hello-world-action").unwrap(),
925 @r#"
926 Local(
927 LocalUses {
928 path: "./.github/actions/hello-world-action",
929 },
930 )
931 "#
932 );
933
934 insta::assert_debug_snapshot!(
936 Uses::parse("checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap_err(),
937 @r#"
938 UsesError(
939 "owner/repo slug is too short: checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
940 )
941 "#
942 );
943
944 insta::assert_debug_snapshot!(
946 Uses::parse("\nactions/checkout@v4 \n").unwrap(),
947 @r#"
948 Repository(
949 RepositoryUses {
950 owner: "actions/checkout@v4",
951 dependent: RepositoryUsesInner {
952 owner: "actions",
953 repo: "checkout",
954 slug: "actions/checkout",
955 subpath: None,
956 git_ref: "v4",
957 },
958 },
959 )
960 "#,
961 );
962
963 insta::assert_debug_snapshot!(
964 Uses::parse("\ndocker://alpine:3.8 \n").unwrap(),
965 @r#"
966 Docker(
967 DockerUses {
968 owner: "alpine:3.8",
969 dependent: DockerUsesInner {
970 registry: None,
971 image: "alpine",
972 tag: Some(
973 "3.8",
974 ),
975 hash: None,
976 },
977 },
978 )
979 "#
980 );
981
982 insta::assert_debug_snapshot!(
983 Uses::parse("\n./.github/workflows/example.yml \n").unwrap(),
984 @r#"
985 Local(
986 LocalUses {
987 path: "./.github/workflows/example.yml",
988 },
989 )
990 "#
991 );
992 }
993
994 #[test]
995 fn test_uses_deser_reusable() {
996 #[derive(Deserialize)]
998 #[serde(transparent)]
999 struct Dummy(#[serde(deserialize_with = "reusable_step_uses")] Uses);
1000
1001 insta::assert_debug_snapshot!(
1002 yaml_serde::from_str::<Dummy>(
1003 "octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
1004 )
1005 .map(|d| d.0)
1006 .unwrap(),
1007 @r#"
1008 Repository(
1009 RepositoryUses {
1010 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89",
1011 dependent: RepositoryUsesInner {
1012 owner: "octo-org",
1013 repo: "this-repo",
1014 slug: "octo-org/this-repo",
1015 subpath: Some(
1016 ".github/workflows/workflow-1.yml",
1017 ),
1018 git_ref: "172239021f7ba04fe7327647b213799853a9eb89",
1019 },
1020 },
1021 )
1022 "#
1023 );
1024
1025 insta::assert_debug_snapshot!(
1026 yaml_serde::from_str::<Dummy>(
1027 "octo-org/this-repo/.github/workflows/workflow-1.yml@notahash"
1028 ).map(|d| d.0).unwrap(),
1029 @r#"
1030 Repository(
1031 RepositoryUses {
1032 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@notahash",
1033 dependent: RepositoryUsesInner {
1034 owner: "octo-org",
1035 repo: "this-repo",
1036 slug: "octo-org/this-repo",
1037 subpath: Some(
1038 ".github/workflows/workflow-1.yml",
1039 ),
1040 git_ref: "notahash",
1041 },
1042 },
1043 )
1044 "#
1045 );
1046
1047 insta::assert_debug_snapshot!(
1048 yaml_serde::from_str::<Dummy>(
1049 "octo-org/this-repo/.github/workflows/workflow-1.yml@abcd"
1050 ).map(|d| d.0).unwrap(),
1051 @r#"
1052 Repository(
1053 RepositoryUses {
1054 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@abcd",
1055 dependent: RepositoryUsesInner {
1056 owner: "octo-org",
1057 repo: "this-repo",
1058 slug: "octo-org/this-repo",
1059 subpath: Some(
1060 ".github/workflows/workflow-1.yml",
1061 ),
1062 git_ref: "abcd",
1063 },
1064 },
1065 )
1066 "#
1067 );
1068
1069 insta::assert_debug_snapshot!(
1071 yaml_serde::from_str::<Dummy>(
1072 "octo-org/this-repo/.github/workflows/workflow-1.yml"
1073 ).map(|d| d.0).unwrap_err(),
1074 @r#"Error("malformed `uses` ref: missing `@<ref>` in octo-org/this-repo/.github/workflows/workflow-1.yml")"#
1075 );
1076
1077 insta::assert_debug_snapshot!(
1079 yaml_serde::from_str::<Dummy>(
1080 "./.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
1081 ).map(|d| d.0).unwrap_err(),
1082 @r#"Error("local reusable workflow reference can't specify `@<ref>`")"#
1083 );
1084
1085 insta::assert_debug_snapshot!(
1087 yaml_serde::from_str::<Dummy>(
1088 ".github/workflows/workflow-1.yml"
1089 ).map(|d| d.0).unwrap_err(),
1090 @r#"Error("malformed `uses` ref: missing `@<ref>` in .github/workflows/workflow-1.yml")"#
1091 );
1092
1093 insta::assert_debug_snapshot!(
1095 yaml_serde::from_str::<Dummy>(
1096 "workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
1097 ).map(|d| d.0).unwrap_err(),
1098 @r#"Error("malformed `uses` ref: owner/repo slug is too short: workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89")"#
1099 );
1100 }
1101}