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_trueish(&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
118#[derive(Deserialize, Debug, PartialEq)]
123#[serde(untagged)]
124enum SoV<T> {
125 One(T),
126 Many(Vec<T>),
127}
128
129impl<T> From<SoV<T>> for Vec<T> {
130 fn from(val: SoV<T>) -> Vec<T> {
131 match val {
132 SoV::One(v) => vec![v],
133 SoV::Many(vs) => vs,
134 }
135 }
136}
137
138pub(crate) fn scalar_or_vector<'de, D, T>(de: D) -> Result<Vec<T>, D::Error>
139where
140 D: Deserializer<'de>,
141 T: Deserialize<'de>,
142{
143 SoV::deserialize(de).map(Into::into)
144}
145
146#[derive(Deserialize, Debug, PartialEq)]
150#[serde(untagged)]
151enum BoS {
152 Bool(bool),
153 String(String),
154}
155
156impl From<BoS> for String {
157 fn from(value: BoS) -> Self {
158 match value {
159 BoS::Bool(b) => b.to_string(),
160 BoS::String(s) => s,
161 }
162 }
163}
164
165#[derive(Deserialize, Serialize, Debug, PartialEq)]
169#[serde(untagged)]
170pub enum If {
171 Bool(bool),
172 Expr(String),
175}
176
177pub(crate) fn bool_is_string<'de, D>(de: D) -> Result<String, D::Error>
178where
179 D: Deserializer<'de>,
180{
181 BoS::deserialize(de).map(Into::into)
182}
183
184fn null_to_default<'de, D, T>(de: D) -> Result<T, D::Error>
185where
186 D: Deserializer<'de>,
187 T: Default + Deserialize<'de>,
188{
189 let key = Option::<T>::deserialize(de)?;
190 Ok(key.unwrap_or_default())
191}
192
193#[derive(Debug, PartialEq)]
195pub struct UsesError(String);
196
197impl fmt::Display for UsesError {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 write!(f, "malformed `uses` ref: {}", self.0)
200 }
201}
202
203#[derive(Debug, PartialEq)]
204pub enum Uses {
205 Local(LocalUses),
207
208 Repository(RepositoryUses),
210
211 Docker(DockerUses),
213}
214
215impl Uses {
216 pub fn parse<'a>(uses: impl Into<Cow<'a, str>>) -> Result<Self, UsesError> {
218 let uses = uses.into();
219 let uses = uses.trim();
220
221 if uses.starts_with("./") {
222 Ok(Self::Local(LocalUses::new(uses)))
223 } else if let Some(image) = uses.strip_prefix("docker://") {
224 Ok(Self::Docker(DockerUses::parse(image)))
225 } else {
226 RepositoryUses::parse(uses).map(Self::Repository)
227 }
228 }
229
230 pub fn raw(&self) -> &str {
232 match self {
233 Uses::Local(local) => &local.path,
234 Uses::Repository(repo) => repo.raw(),
235 Uses::Docker(docker) => docker.raw(),
236 }
237 }
238}
239
240#[derive(Debug, PartialEq)]
242#[non_exhaustive]
243pub struct LocalUses {
244 pub path: String,
245}
246
247impl LocalUses {
248 fn new(path: impl Into<String>) -> Self {
249 LocalUses { path: path.into() }
250 }
251}
252
253#[derive(Debug, PartialEq)]
254struct RepositoryUsesInner<'a> {
255 owner: &'a str,
257 repo: &'a str,
259 slug: &'a str,
261 subpath: Option<&'a str>,
263 git_ref: &'a str,
265}
266
267impl<'a> RepositoryUsesInner<'a> {
268 fn from_str(uses: &'a str) -> Result<Self, UsesError> {
269 let uses = uses.trim();
271
272 let (path, git_ref) = match uses.rsplit_once('@') {
275 Some((path, git_ref)) => (path, git_ref),
276 None => return Err(UsesError(format!("missing `@<ref>` in {uses}"))),
277 };
278
279 let mut components = path.splitn(3, '/');
280
281 if let Some(owner) = components.next()
282 && let Some(repo) = components.next()
283 {
284 let subpath = components.next();
285
286 let slug = if subpath.is_none() {
287 path
288 } else {
289 &path[..owner.len() + 1 + repo.len()]
290 };
291
292 Ok(RepositoryUsesInner {
293 owner,
294 repo,
295 slug,
296 subpath,
297 git_ref,
298 })
299 } else {
300 Err(UsesError(format!("owner/repo slug is too short: {uses}")))
301 }
302 }
303}
304
305self_cell!(
306 pub struct RepositoryUses {
308 owner: String,
309
310 #[covariant]
311 dependent: RepositoryUsesInner,
312 }
313
314 impl {Debug, PartialEq}
315);
316
317impl Display for RepositoryUses {
318 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319 write!(f, "{}", self.raw())
320 }
321}
322
323impl RepositoryUses {
324 pub fn parse(uses: impl Into<String>) -> Result<Self, UsesError> {
326 RepositoryUses::try_new(uses.into(), |s| {
327 let inner = RepositoryUsesInner::from_str(s)?;
328 Ok(inner)
329 })
330 }
331
332 pub fn raw(&self) -> &str {
334 self.borrow_owner()
335 }
336
337 pub fn owner(&self) -> &str {
339 self.borrow_dependent().owner
340 }
341
342 pub fn repo(&self) -> &str {
344 self.borrow_dependent().repo
345 }
346
347 pub fn slug(&self) -> &str {
349 self.borrow_dependent().slug
350 }
351
352 pub fn subpath(&self) -> Option<&str> {
354 self.borrow_dependent().subpath
355 }
356
357 pub fn git_ref(&self) -> &str {
359 self.borrow_dependent().git_ref
360 }
361}
362
363#[derive(Debug, PartialEq)]
364#[non_exhaustive]
365pub struct DockerUsesInner<'a> {
366 registry: Option<&'a str>,
368 image: &'a str,
370 tag: Option<&'a str>,
372 hash: Option<&'a str>,
374}
375
376impl<'a> DockerUsesInner<'a> {
377 fn is_registry(registry: &str) -> bool {
378 registry == "localhost" || registry.contains('.') || registry.contains(':')
380 }
381
382 fn from_str(uses: &'a str) -> Self {
383 let uses = uses.trim();
385
386 let (registry, image) = match uses.split_once('/') {
387 Some((registry, image)) if Self::is_registry(registry) => (Some(registry), image),
388 _ => (None, uses),
389 };
390
391 if let Some(at_pos) = image.find('@') {
395 let (image, hash) = image.split_at(at_pos);
396
397 let hash = if hash.is_empty() {
398 None
399 } else {
400 Some(&hash[1..])
401 };
402
403 DockerUsesInner {
404 registry,
405 image,
406 tag: None,
407 hash,
408 }
409 } else {
410 let (image, tag) = match image.split_once(':') {
411 Some((image, "")) => (image, None),
412 Some((image, tag)) => (image, Some(tag)),
413 _ => (image, None),
414 };
415
416 DockerUsesInner {
417 registry,
418 image,
419 tag,
420 hash: None,
421 }
422 }
423 }
424}
425
426self_cell!(
427 pub struct DockerUses {
429 owner: String,
430
431 #[covariant]
432 dependent: DockerUsesInner,
433 }
434
435 impl {Debug, PartialEq}
436);
437
438impl DockerUses {
439 pub fn parse(uses: impl Into<String>) -> Self {
441 DockerUses::new(uses.into(), |s| DockerUsesInner::from_str(s))
442 }
443
444 pub fn raw(&self) -> &str {
446 self.borrow_owner()
447 }
448
449 pub fn registry(&self) -> Option<&str> {
451 self.borrow_dependent().registry
452 }
453
454 pub fn image(&self) -> &str {
456 self.borrow_dependent().image
457 }
458
459 pub fn tag(&self) -> Option<&str> {
461 self.borrow_dependent().tag
462 }
463
464 pub fn hash(&self) -> Option<&str> {
466 self.borrow_dependent().hash
467 }
468}
469
470impl<'de> Deserialize<'de> for DockerUses {
471 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
472 where
473 D: Deserializer<'de>,
474 {
475 let uses = <Cow<'de, str>>::deserialize(deserializer)?;
476 Ok(DockerUses::parse(uses))
477 }
478}
479
480pub(crate) fn custom_error<'de, D>(msg: impl Display) -> D::Error
486where
487 D: Deserializer<'de>,
488{
489 let msg = msg.to_string();
490 tracing::error!(msg);
491 de::Error::custom(msg)
492}
493
494pub(crate) fn step_uses<'de, D>(de: D) -> Result<Uses, D::Error>
496where
497 D: Deserializer<'de>,
498{
499 let uses = <Cow<'de, str>>::deserialize(de)?;
500 Uses::parse(uses).map_err(custom_error::<D>)
501}
502
503pub(crate) fn reusable_step_uses<'de, D>(de: D) -> Result<Uses, D::Error>
505where
506 D: Deserializer<'de>,
507{
508 let uses = step_uses(de)?;
509
510 match uses {
511 Uses::Repository(_) => Ok(uses),
512 Uses::Local(ref local) => {
513 if local.path.contains('@') {
518 Err(custom_error::<D>(
519 "local reusable workflow reference can't specify `@<ref>`",
520 ))
521 } else {
522 Ok(uses)
523 }
524 }
525 Uses::Docker(_) => Err(custom_error::<D>(
527 "docker action invalid in reusable workflow `uses`",
528 )),
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use indexmap::IndexMap;
535 use serde::Deserialize;
536
537 use crate::common::{BasePermission, Env, EnvValue, Permission};
538
539 use super::{Permissions, Uses, reusable_step_uses};
540
541 #[test]
542 fn test_permissions() {
543 assert_eq!(
544 serde_yaml::from_str::<Permissions>("read-all").unwrap(),
545 Permissions::Base(BasePermission::ReadAll)
546 );
547
548 let perm = "security-events: write";
549 assert_eq!(
550 serde_yaml::from_str::<Permissions>(perm).unwrap(),
551 Permissions::Explicit(IndexMap::from([(
552 "security-events".into(),
553 Permission::Write
554 )]))
555 );
556 }
557
558 #[test]
559 fn test_env_empty_value() {
560 let env = "foo:";
561 assert_eq!(
562 serde_yaml::from_str::<Env>(env).unwrap()["foo"],
563 EnvValue::String("".into())
564 );
565 }
566
567 #[test]
568 fn test_env_value_csharp_trueish() {
569 let vectors = [
570 (EnvValue::Boolean(true), true),
571 (EnvValue::Boolean(false), false),
572 (EnvValue::String("true".to_string()), true),
573 (EnvValue::String("TRUE".to_string()), true),
574 (EnvValue::String("TrUe".to_string()), true),
575 (EnvValue::String(" true ".to_string()), true),
576 (EnvValue::String(" \n\r\t True\n\n".to_string()), true),
577 (EnvValue::String("false".to_string()), false),
578 (EnvValue::String("1".to_string()), false),
579 (EnvValue::String("yes".to_string()), false),
580 (EnvValue::String("on".to_string()), false),
581 (EnvValue::String("random".to_string()), false),
582 (EnvValue::Number(1.0), false),
583 (EnvValue::Number(0.0), false),
584 (EnvValue::Number(666.0), false),
585 ];
586
587 for (val, expected) in vectors {
588 assert_eq!(val.csharp_trueish(), expected, "failed for {val:?}");
589 }
590 }
591
592 #[test]
593 fn test_uses_parses() {
594 insta::assert_debug_snapshot!(
596 Uses::parse("actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
597 @r#"
598 Repository(
599 RepositoryUses {
600 owner: "actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
601 dependent: RepositoryUsesInner {
602 owner: "actions",
603 repo: "checkout",
604 slug: "actions/checkout",
605 subpath: None,
606 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
607 },
608 },
609 )
610 "#,
611 );
612
613 insta::assert_debug_snapshot!(
615 Uses::parse("actions/aws/ec2@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
616 @r#"
617 Repository(
618 RepositoryUses {
619 owner: "actions/aws/ec2@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
620 dependent: RepositoryUsesInner {
621 owner: "actions",
622 repo: "aws",
623 slug: "actions/aws",
624 subpath: Some(
625 "ec2",
626 ),
627 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
628 },
629 },
630 )
631 "#
632 );
633
634 insta::assert_debug_snapshot!(
636 Uses::parse("example/foo/bar/baz/quux@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
637 @r#"
638 Repository(
639 RepositoryUses {
640 owner: "example/foo/bar/baz/quux@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
641 dependent: RepositoryUsesInner {
642 owner: "example",
643 repo: "foo",
644 slug: "example/foo",
645 subpath: Some(
646 "bar/baz/quux",
647 ),
648 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
649 },
650 },
651 )
652 "#
653 );
654
655 insta::assert_debug_snapshot!(
657 Uses::parse("actions/checkout@v4").unwrap(),
658 @r#"
659 Repository(
660 RepositoryUses {
661 owner: "actions/checkout@v4",
662 dependent: RepositoryUsesInner {
663 owner: "actions",
664 repo: "checkout",
665 slug: "actions/checkout",
666 subpath: None,
667 git_ref: "v4",
668 },
669 },
670 )
671 "#
672 );
673
674 insta::assert_debug_snapshot!(
675 Uses::parse("actions/checkout@abcd").unwrap(),
676 @r#"
677 Repository(
678 RepositoryUses {
679 owner: "actions/checkout@abcd",
680 dependent: RepositoryUsesInner {
681 owner: "actions",
682 repo: "checkout",
683 slug: "actions/checkout",
684 subpath: None,
685 git_ref: "abcd",
686 },
687 },
688 )
689 "#
690 );
691
692 insta::assert_debug_snapshot!(
694 Uses::parse("actions/checkout").unwrap_err(),
695 @r#"
696 UsesError(
697 "missing `@<ref>` in actions/checkout",
698 )
699 "#
700 );
701
702 insta::assert_debug_snapshot!(
704 Uses::parse("docker://alpine:3.8").unwrap(),
705 @r#"
706 Docker(
707 DockerUses {
708 owner: "alpine:3.8",
709 dependent: DockerUsesInner {
710 registry: None,
711 image: "alpine",
712 tag: Some(
713 "3.8",
714 ),
715 hash: None,
716 },
717 },
718 )
719 "#
720 );
721
722 insta::assert_debug_snapshot!(
724 Uses::parse("docker://localhost/alpine:3.8").unwrap(),
725 @r#"
726 Docker(
727 DockerUses {
728 owner: "localhost/alpine:3.8",
729 dependent: DockerUsesInner {
730 registry: Some(
731 "localhost",
732 ),
733 image: "alpine",
734 tag: Some(
735 "3.8",
736 ),
737 hash: None,
738 },
739 },
740 )
741 "#
742 );
743
744 insta::assert_debug_snapshot!(
746 Uses::parse("docker://localhost:1337/alpine:3.8").unwrap(),
747 @r#"
748 Docker(
749 DockerUses {
750 owner: "localhost:1337/alpine:3.8",
751 dependent: DockerUsesInner {
752 registry: Some(
753 "localhost:1337",
754 ),
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://ghcr.io/foo/alpine:3.8").unwrap(),
769 @r#"
770 Docker(
771 DockerUses {
772 owner: "ghcr.io/foo/alpine:3.8",
773 dependent: DockerUsesInner {
774 registry: Some(
775 "ghcr.io",
776 ),
777 image: "foo/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://ghcr.io/foo/alpine").unwrap(),
791 @r#"
792 Docker(
793 DockerUses {
794 owner: "ghcr.io/foo/alpine",
795 dependent: DockerUsesInner {
796 registry: Some(
797 "ghcr.io",
798 ),
799 image: "foo/alpine",
800 tag: None,
801 hash: None,
802 },
803 },
804 )
805 "#
806 );
807
808 insta::assert_debug_snapshot!(
810 Uses::parse("docker://ghcr.io/foo/alpine:").unwrap(),
811 @r#"
812 Docker(
813 DockerUses {
814 owner: "ghcr.io/foo/alpine:",
815 dependent: DockerUsesInner {
816 registry: Some(
817 "ghcr.io",
818 ),
819 image: "foo/alpine",
820 tag: None,
821 hash: None,
822 },
823 },
824 )
825 "#
826 );
827
828 insta::assert_debug_snapshot!(
830 Uses::parse("docker://alpine").unwrap(),
831 @r#"
832 Docker(
833 DockerUses {
834 owner: "alpine",
835 dependent: DockerUsesInner {
836 registry: None,
837 image: "alpine",
838 tag: None,
839 hash: None,
840 },
841 },
842 )
843 "#
844 );
845
846 insta::assert_debug_snapshot!(
848 Uses::parse("docker://alpine@hash").unwrap(),
849 @r#"
850 Docker(
851 DockerUses {
852 owner: "alpine@hash",
853 dependent: DockerUsesInner {
854 registry: None,
855 image: "alpine",
856 tag: None,
857 hash: Some(
858 "hash",
859 ),
860 },
861 },
862 )
863 "#
864 );
865
866 insta::assert_debug_snapshot!(
868 Uses::parse("./.github/actions/hello-world-action@172239021f7ba04fe7327647b213799853a9eb89").unwrap(),
869 @r#"
870 Local(
871 LocalUses {
872 path: "./.github/actions/hello-world-action@172239021f7ba04fe7327647b213799853a9eb89",
873 },
874 )
875 "#
876 );
877
878 insta::assert_debug_snapshot!(
880 Uses::parse("./.github/actions/hello-world-action").unwrap(),
881 @r#"
882 Local(
883 LocalUses {
884 path: "./.github/actions/hello-world-action",
885 },
886 )
887 "#
888 );
889
890 insta::assert_debug_snapshot!(
892 Uses::parse("checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap_err(),
893 @r#"
894 UsesError(
895 "owner/repo slug is too short: checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
896 )
897 "#
898 );
899
900 insta::assert_debug_snapshot!(
902 Uses::parse("\nactions/checkout@v4 \n").unwrap(),
903 @r#"
904 Repository(
905 RepositoryUses {
906 owner: "actions/checkout@v4",
907 dependent: RepositoryUsesInner {
908 owner: "actions",
909 repo: "checkout",
910 slug: "actions/checkout",
911 subpath: None,
912 git_ref: "v4",
913 },
914 },
915 )
916 "#,
917 );
918
919 insta::assert_debug_snapshot!(
920 Uses::parse("\ndocker://alpine:3.8 \n").unwrap(),
921 @r#"
922 Docker(
923 DockerUses {
924 owner: "alpine:3.8",
925 dependent: DockerUsesInner {
926 registry: None,
927 image: "alpine",
928 tag: Some(
929 "3.8",
930 ),
931 hash: None,
932 },
933 },
934 )
935 "#
936 );
937
938 insta::assert_debug_snapshot!(
939 Uses::parse("\n./.github/workflows/example.yml \n").unwrap(),
940 @r#"
941 Local(
942 LocalUses {
943 path: "./.github/workflows/example.yml",
944 },
945 )
946 "#
947 );
948 }
949
950 #[test]
951 fn test_uses_deser_reusable() {
952 #[derive(Deserialize)]
954 #[serde(transparent)]
955 struct Dummy(#[serde(deserialize_with = "reusable_step_uses")] Uses);
956
957 insta::assert_debug_snapshot!(
958 serde_yaml::from_str::<Dummy>(
959 "octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
960 )
961 .map(|d| d.0)
962 .unwrap(),
963 @r#"
964 Repository(
965 RepositoryUses {
966 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89",
967 dependent: RepositoryUsesInner {
968 owner: "octo-org",
969 repo: "this-repo",
970 slug: "octo-org/this-repo",
971 subpath: Some(
972 ".github/workflows/workflow-1.yml",
973 ),
974 git_ref: "172239021f7ba04fe7327647b213799853a9eb89",
975 },
976 },
977 )
978 "#
979 );
980
981 insta::assert_debug_snapshot!(
982 serde_yaml::from_str::<Dummy>(
983 "octo-org/this-repo/.github/workflows/workflow-1.yml@notahash"
984 ).map(|d| d.0).unwrap(),
985 @r#"
986 Repository(
987 RepositoryUses {
988 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@notahash",
989 dependent: RepositoryUsesInner {
990 owner: "octo-org",
991 repo: "this-repo",
992 slug: "octo-org/this-repo",
993 subpath: Some(
994 ".github/workflows/workflow-1.yml",
995 ),
996 git_ref: "notahash",
997 },
998 },
999 )
1000 "#
1001 );
1002
1003 insta::assert_debug_snapshot!(
1004 serde_yaml::from_str::<Dummy>(
1005 "octo-org/this-repo/.github/workflows/workflow-1.yml@abcd"
1006 ).map(|d| d.0).unwrap(),
1007 @r#"
1008 Repository(
1009 RepositoryUses {
1010 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@abcd",
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: "abcd",
1019 },
1020 },
1021 )
1022 "#
1023 );
1024
1025 insta::assert_debug_snapshot!(
1027 serde_yaml::from_str::<Dummy>(
1028 "octo-org/this-repo/.github/workflows/workflow-1.yml"
1029 ).map(|d| d.0).unwrap_err(),
1030 @r#"Error("malformed `uses` ref: missing `@<ref>` in octo-org/this-repo/.github/workflows/workflow-1.yml")"#
1031 );
1032
1033 insta::assert_debug_snapshot!(
1035 serde_yaml::from_str::<Dummy>(
1036 "./.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
1037 ).map(|d| d.0).unwrap_err(),
1038 @r#"Error("local reusable workflow reference can't specify `@<ref>`")"#
1039 );
1040
1041 insta::assert_debug_snapshot!(
1043 serde_yaml::from_str::<Dummy>(
1044 ".github/workflows/workflow-1.yml"
1045 ).map(|d| d.0).unwrap_err(),
1046 @r#"Error("malformed `uses` ref: missing `@<ref>` in .github/workflows/workflow-1.yml")"#
1047 );
1048
1049 insta::assert_debug_snapshot!(
1051 serde_yaml::from_str::<Dummy>(
1052 "workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
1053 ).map(|d| d.0).unwrap_err(),
1054 @r#"Error("malformed `uses` ref: owner/repo slug is too short: workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89")"#
1055 );
1056 }
1057}