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(Serialize, Debug, PartialEq)]
174pub enum If {
175 Bool(bool),
176 Expr(String),
179}
180
181impl<'de> Deserialize<'de> for If {
182 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
183 where
184 D: Deserializer<'de>,
185 {
186 #[derive(Deserialize)]
189 #[serde(untagged)]
190 enum RawIf {
191 Bool(bool),
192 Int(i64),
193 Float(f64),
194 Expr(String),
195 }
196
197 match RawIf::deserialize(deserializer)? {
198 RawIf::Bool(b) => Ok(If::Bool(b)),
199 RawIf::Int(n) => Ok(If::Bool(n != 0)),
200 RawIf::Float(f) => Ok(If::Bool(f != 0.0 && !f.is_nan())),
201 RawIf::Expr(s) => Ok(If::Expr(s)),
202 }
203 }
204}
205
206pub(crate) fn bool_is_string<'de, D>(de: D) -> Result<String, D::Error>
207where
208 D: Deserializer<'de>,
209{
210 BoS::deserialize(de).map(Into::into)
211}
212
213fn null_to_default<'de, D, T>(de: D) -> Result<T, D::Error>
214where
215 D: Deserializer<'de>,
216 T: Default + Deserialize<'de>,
217{
218 let key = Option::<T>::deserialize(de)?;
219 Ok(key.unwrap_or_default())
220}
221
222#[derive(Debug, PartialEq)]
224pub struct UsesError(String);
225
226impl fmt::Display for UsesError {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 write!(f, "malformed `uses` ref: {}", self.0)
229 }
230}
231
232#[derive(Debug, PartialEq)]
233pub enum Uses {
234 Local(LocalUses),
236
237 Repository(RepositoryUses),
239
240 Docker(DockerUses),
242}
243
244impl Uses {
245 pub fn parse<'a>(uses: impl Into<Cow<'a, str>>) -> Result<Self, UsesError> {
247 let uses = uses.into();
248 let uses = uses.trim();
249
250 if uses.starts_with("./") {
251 Ok(Self::Local(LocalUses::new(uses)))
252 } else if let Some(image) = uses.strip_prefix("docker://") {
253 Ok(Self::Docker(DockerUses::parse(image)))
254 } else {
255 RepositoryUses::parse(uses).map(Self::Repository)
256 }
257 }
258
259 pub fn raw(&self) -> &str {
261 match self {
262 Uses::Local(local) => &local.path,
263 Uses::Repository(repo) => repo.raw(),
264 Uses::Docker(docker) => docker.raw(),
265 }
266 }
267}
268
269#[derive(Debug, PartialEq)]
271#[non_exhaustive]
272pub struct LocalUses {
273 pub path: String,
274}
275
276impl LocalUses {
277 fn new(path: impl Into<String>) -> Self {
278 LocalUses { path: path.into() }
279 }
280}
281
282#[derive(Debug, PartialEq)]
283struct RepositoryUsesInner<'a> {
284 owner: &'a str,
286 repo: &'a str,
288 slug: &'a str,
290 subpath: Option<&'a str>,
292 git_ref: &'a str,
294}
295
296impl<'a> RepositoryUsesInner<'a> {
297 fn from_str(uses: &'a str) -> Result<Self, UsesError> {
298 let uses = uses.trim();
300
301 let (path, git_ref) = match uses.rsplit_once('@') {
304 Some((path, git_ref)) => (path, git_ref),
305 None => return Err(UsesError(format!("missing `@<ref>` in {uses}"))),
306 };
307
308 let mut components = path.splitn(3, '/');
309
310 if let Some(owner) = components.next()
311 && let Some(repo) = components.next()
312 {
313 let subpath = components.next();
314
315 let slug = if subpath.is_none() {
316 path
317 } else {
318 &path[..owner.len() + 1 + repo.len()]
319 };
320
321 Ok(RepositoryUsesInner {
322 owner,
323 repo,
324 slug,
325 subpath,
326 git_ref,
327 })
328 } else {
329 Err(UsesError(format!("owner/repo slug is too short: {uses}")))
330 }
331 }
332}
333
334self_cell!(
335 pub struct RepositoryUses {
337 owner: String,
338
339 #[covariant]
340 dependent: RepositoryUsesInner,
341 }
342
343 impl {Debug, PartialEq}
344);
345
346impl Display for RepositoryUses {
347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 write!(f, "{}", self.raw())
349 }
350}
351
352impl RepositoryUses {
353 pub fn parse(uses: impl Into<String>) -> Result<Self, UsesError> {
355 RepositoryUses::try_new(uses.into(), |s| {
356 let inner = RepositoryUsesInner::from_str(s)?;
357 Ok(inner)
358 })
359 }
360
361 pub fn raw(&self) -> &str {
363 self.borrow_owner()
364 }
365
366 pub fn owner(&self) -> &str {
368 self.borrow_dependent().owner
369 }
370
371 pub fn repo(&self) -> &str {
373 self.borrow_dependent().repo
374 }
375
376 pub fn slug(&self) -> &str {
378 self.borrow_dependent().slug
379 }
380
381 pub fn subpath(&self) -> Option<&str> {
383 self.borrow_dependent().subpath
384 }
385
386 pub fn git_ref(&self) -> &str {
388 self.borrow_dependent().git_ref
389 }
390}
391
392#[derive(Debug, PartialEq)]
393#[non_exhaustive]
394pub struct DockerUsesInner<'a> {
395 registry: Option<&'a str>,
397 image: &'a str,
399 tag: Option<&'a str>,
401 hash: Option<&'a str>,
403}
404
405impl<'a> DockerUsesInner<'a> {
406 fn is_registry(registry: &str) -> bool {
407 registry == "localhost" || registry.contains('.') || registry.contains(':')
409 }
410
411 fn from_str(uses: &'a str) -> Self {
412 let uses = uses.trim();
414
415 let (registry, image) = match uses.split_once('/') {
416 Some((registry, image)) if Self::is_registry(registry) => (Some(registry), image),
417 _ => (None, uses),
418 };
419
420 if let Some(at_pos) = image.find('@') {
424 let (image, hash) = image.split_at(at_pos);
425
426 let hash = if hash.is_empty() {
427 None
428 } else {
429 Some(&hash[1..])
430 };
431
432 DockerUsesInner {
433 registry,
434 image,
435 tag: None,
436 hash,
437 }
438 } else {
439 let (image, tag) = match image.split_once(':') {
440 Some((image, "")) => (image, None),
441 Some((image, tag)) => (image, Some(tag)),
442 _ => (image, None),
443 };
444
445 DockerUsesInner {
446 registry,
447 image,
448 tag,
449 hash: None,
450 }
451 }
452 }
453}
454
455self_cell!(
456 pub struct DockerUses {
458 owner: String,
459
460 #[covariant]
461 dependent: DockerUsesInner,
462 }
463
464 impl {Debug, PartialEq}
465);
466
467impl DockerUses {
468 pub fn parse(uses: impl Into<String>) -> Self {
470 DockerUses::new(uses.into(), |s| DockerUsesInner::from_str(s))
471 }
472
473 pub fn raw(&self) -> &str {
475 self.borrow_owner()
476 }
477
478 pub fn registry(&self) -> Option<&str> {
480 self.borrow_dependent().registry
481 }
482
483 pub fn image(&self) -> &str {
485 self.borrow_dependent().image
486 }
487
488 pub fn tag(&self) -> Option<&str> {
490 self.borrow_dependent().tag
491 }
492
493 pub fn hash(&self) -> Option<&str> {
495 self.borrow_dependent().hash
496 }
497}
498
499impl<'de> Deserialize<'de> for DockerUses {
500 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
501 where
502 D: Deserializer<'de>,
503 {
504 let uses = <Cow<'de, str>>::deserialize(deserializer)?;
505 Ok(DockerUses::parse(uses))
506 }
507}
508
509pub(crate) fn custom_error<'de, D>(msg: impl Display) -> D::Error
515where
516 D: Deserializer<'de>,
517{
518 let msg = msg.to_string();
519 tracing::error!(msg);
520 de::Error::custom(msg)
521}
522
523pub(crate) fn step_uses<'de, D>(de: D) -> Result<Uses, D::Error>
525where
526 D: Deserializer<'de>,
527{
528 let uses = <Cow<'de, str>>::deserialize(de)?;
529 Uses::parse(uses).map_err(custom_error::<D>)
530}
531
532pub(crate) fn reusable_step_uses<'de, D>(de: D) -> Result<Uses, D::Error>
534where
535 D: Deserializer<'de>,
536{
537 let uses = step_uses(de)?;
538
539 match uses {
540 Uses::Repository(_) => Ok(uses),
541 Uses::Local(ref local) => {
542 if local.path.contains('@') {
547 Err(custom_error::<D>(
548 "local reusable workflow reference can't specify `@<ref>`",
549 ))
550 } else {
551 Ok(uses)
552 }
553 }
554 Uses::Docker(_) => Err(custom_error::<D>(
556 "docker action invalid in reusable workflow `uses`",
557 )),
558 }
559}
560
561#[cfg(test)]
562mod tests {
563 use indexmap::IndexMap;
564 use serde::Deserialize;
565
566 use crate::common::{BasePermission, Env, EnvValue, Permission};
567
568 use super::{Permissions, Uses, reusable_step_uses};
569
570 #[test]
571 fn test_permissions() {
572 assert_eq!(
573 serde_yaml::from_str::<Permissions>("read-all").unwrap(),
574 Permissions::Base(BasePermission::ReadAll)
575 );
576
577 let perm = "security-events: write";
578 assert_eq!(
579 serde_yaml::from_str::<Permissions>(perm).unwrap(),
580 Permissions::Explicit(IndexMap::from([(
581 "security-events".into(),
582 Permission::Write
583 )]))
584 );
585 }
586
587 #[test]
588 fn test_env_empty_value() {
589 let env = "foo:";
590 assert_eq!(
591 serde_yaml::from_str::<Env>(env).unwrap()["foo"],
592 EnvValue::String("".into())
593 );
594 }
595
596 #[test]
597 fn test_env_value_csharp_trueish() {
598 let vectors = [
599 (EnvValue::Boolean(true), true),
600 (EnvValue::Boolean(false), false),
601 (EnvValue::String("true".to_string()), true),
602 (EnvValue::String("TRUE".to_string()), true),
603 (EnvValue::String("TrUe".to_string()), true),
604 (EnvValue::String(" true ".to_string()), true),
605 (EnvValue::String(" \n\r\t True\n\n".to_string()), true),
606 (EnvValue::String("false".to_string()), false),
607 (EnvValue::String("1".to_string()), false),
608 (EnvValue::String("yes".to_string()), false),
609 (EnvValue::String("on".to_string()), false),
610 (EnvValue::String("random".to_string()), false),
611 (EnvValue::Number(1.0), false),
612 (EnvValue::Number(0.0), false),
613 (EnvValue::Number(666.0), false),
614 ];
615
616 for (val, expected) in vectors {
617 assert_eq!(val.csharp_trueish(), expected, "failed for {val:?}");
618 }
619 }
620
621 #[test]
622 fn test_uses_parses() {
623 insta::assert_debug_snapshot!(
625 Uses::parse("actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
626 @r#"
627 Repository(
628 RepositoryUses {
629 owner: "actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
630 dependent: RepositoryUsesInner {
631 owner: "actions",
632 repo: "checkout",
633 slug: "actions/checkout",
634 subpath: None,
635 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
636 },
637 },
638 )
639 "#,
640 );
641
642 insta::assert_debug_snapshot!(
644 Uses::parse("actions/aws/ec2@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
645 @r#"
646 Repository(
647 RepositoryUses {
648 owner: "actions/aws/ec2@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
649 dependent: RepositoryUsesInner {
650 owner: "actions",
651 repo: "aws",
652 slug: "actions/aws",
653 subpath: Some(
654 "ec2",
655 ),
656 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
657 },
658 },
659 )
660 "#
661 );
662
663 insta::assert_debug_snapshot!(
665 Uses::parse("example/foo/bar/baz/quux@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
666 @r#"
667 Repository(
668 RepositoryUses {
669 owner: "example/foo/bar/baz/quux@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
670 dependent: RepositoryUsesInner {
671 owner: "example",
672 repo: "foo",
673 slug: "example/foo",
674 subpath: Some(
675 "bar/baz/quux",
676 ),
677 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
678 },
679 },
680 )
681 "#
682 );
683
684 insta::assert_debug_snapshot!(
686 Uses::parse("actions/checkout@v4").unwrap(),
687 @r#"
688 Repository(
689 RepositoryUses {
690 owner: "actions/checkout@v4",
691 dependent: RepositoryUsesInner {
692 owner: "actions",
693 repo: "checkout",
694 slug: "actions/checkout",
695 subpath: None,
696 git_ref: "v4",
697 },
698 },
699 )
700 "#
701 );
702
703 insta::assert_debug_snapshot!(
704 Uses::parse("actions/checkout@abcd").unwrap(),
705 @r#"
706 Repository(
707 RepositoryUses {
708 owner: "actions/checkout@abcd",
709 dependent: RepositoryUsesInner {
710 owner: "actions",
711 repo: "checkout",
712 slug: "actions/checkout",
713 subpath: None,
714 git_ref: "abcd",
715 },
716 },
717 )
718 "#
719 );
720
721 insta::assert_debug_snapshot!(
723 Uses::parse("actions/checkout").unwrap_err(),
724 @r#"
725 UsesError(
726 "missing `@<ref>` in actions/checkout",
727 )
728 "#
729 );
730
731 insta::assert_debug_snapshot!(
733 Uses::parse("docker://alpine:3.8").unwrap(),
734 @r#"
735 Docker(
736 DockerUses {
737 owner: "alpine:3.8",
738 dependent: DockerUsesInner {
739 registry: None,
740 image: "alpine",
741 tag: Some(
742 "3.8",
743 ),
744 hash: None,
745 },
746 },
747 )
748 "#
749 );
750
751 insta::assert_debug_snapshot!(
753 Uses::parse("docker://localhost/alpine:3.8").unwrap(),
754 @r#"
755 Docker(
756 DockerUses {
757 owner: "localhost/alpine:3.8",
758 dependent: DockerUsesInner {
759 registry: Some(
760 "localhost",
761 ),
762 image: "alpine",
763 tag: Some(
764 "3.8",
765 ),
766 hash: None,
767 },
768 },
769 )
770 "#
771 );
772
773 insta::assert_debug_snapshot!(
775 Uses::parse("docker://localhost:1337/alpine:3.8").unwrap(),
776 @r#"
777 Docker(
778 DockerUses {
779 owner: "localhost:1337/alpine:3.8",
780 dependent: DockerUsesInner {
781 registry: Some(
782 "localhost:1337",
783 ),
784 image: "alpine",
785 tag: Some(
786 "3.8",
787 ),
788 hash: None,
789 },
790 },
791 )
792 "#
793 );
794
795 insta::assert_debug_snapshot!(
797 Uses::parse("docker://ghcr.io/foo/alpine:3.8").unwrap(),
798 @r#"
799 Docker(
800 DockerUses {
801 owner: "ghcr.io/foo/alpine:3.8",
802 dependent: DockerUsesInner {
803 registry: Some(
804 "ghcr.io",
805 ),
806 image: "foo/alpine",
807 tag: Some(
808 "3.8",
809 ),
810 hash: None,
811 },
812 },
813 )
814 "#
815 );
816
817 insta::assert_debug_snapshot!(
819 Uses::parse("docker://ghcr.io/foo/alpine").unwrap(),
820 @r#"
821 Docker(
822 DockerUses {
823 owner: "ghcr.io/foo/alpine",
824 dependent: DockerUsesInner {
825 registry: Some(
826 "ghcr.io",
827 ),
828 image: "foo/alpine",
829 tag: None,
830 hash: None,
831 },
832 },
833 )
834 "#
835 );
836
837 insta::assert_debug_snapshot!(
839 Uses::parse("docker://ghcr.io/foo/alpine:").unwrap(),
840 @r#"
841 Docker(
842 DockerUses {
843 owner: "ghcr.io/foo/alpine:",
844 dependent: DockerUsesInner {
845 registry: Some(
846 "ghcr.io",
847 ),
848 image: "foo/alpine",
849 tag: None,
850 hash: None,
851 },
852 },
853 )
854 "#
855 );
856
857 insta::assert_debug_snapshot!(
859 Uses::parse("docker://alpine").unwrap(),
860 @r#"
861 Docker(
862 DockerUses {
863 owner: "alpine",
864 dependent: DockerUsesInner {
865 registry: None,
866 image: "alpine",
867 tag: None,
868 hash: None,
869 },
870 },
871 )
872 "#
873 );
874
875 insta::assert_debug_snapshot!(
877 Uses::parse("docker://alpine@hash").unwrap(),
878 @r#"
879 Docker(
880 DockerUses {
881 owner: "alpine@hash",
882 dependent: DockerUsesInner {
883 registry: None,
884 image: "alpine",
885 tag: None,
886 hash: Some(
887 "hash",
888 ),
889 },
890 },
891 )
892 "#
893 );
894
895 insta::assert_debug_snapshot!(
897 Uses::parse("./.github/actions/hello-world-action@172239021f7ba04fe7327647b213799853a9eb89").unwrap(),
898 @r#"
899 Local(
900 LocalUses {
901 path: "./.github/actions/hello-world-action@172239021f7ba04fe7327647b213799853a9eb89",
902 },
903 )
904 "#
905 );
906
907 insta::assert_debug_snapshot!(
909 Uses::parse("./.github/actions/hello-world-action").unwrap(),
910 @r#"
911 Local(
912 LocalUses {
913 path: "./.github/actions/hello-world-action",
914 },
915 )
916 "#
917 );
918
919 insta::assert_debug_snapshot!(
921 Uses::parse("checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap_err(),
922 @r#"
923 UsesError(
924 "owner/repo slug is too short: checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
925 )
926 "#
927 );
928
929 insta::assert_debug_snapshot!(
931 Uses::parse("\nactions/checkout@v4 \n").unwrap(),
932 @r#"
933 Repository(
934 RepositoryUses {
935 owner: "actions/checkout@v4",
936 dependent: RepositoryUsesInner {
937 owner: "actions",
938 repo: "checkout",
939 slug: "actions/checkout",
940 subpath: None,
941 git_ref: "v4",
942 },
943 },
944 )
945 "#,
946 );
947
948 insta::assert_debug_snapshot!(
949 Uses::parse("\ndocker://alpine:3.8 \n").unwrap(),
950 @r#"
951 Docker(
952 DockerUses {
953 owner: "alpine:3.8",
954 dependent: DockerUsesInner {
955 registry: None,
956 image: "alpine",
957 tag: Some(
958 "3.8",
959 ),
960 hash: None,
961 },
962 },
963 )
964 "#
965 );
966
967 insta::assert_debug_snapshot!(
968 Uses::parse("\n./.github/workflows/example.yml \n").unwrap(),
969 @r#"
970 Local(
971 LocalUses {
972 path: "./.github/workflows/example.yml",
973 },
974 )
975 "#
976 );
977 }
978
979 #[test]
980 fn test_uses_deser_reusable() {
981 #[derive(Deserialize)]
983 #[serde(transparent)]
984 struct Dummy(#[serde(deserialize_with = "reusable_step_uses")] Uses);
985
986 insta::assert_debug_snapshot!(
987 serde_yaml::from_str::<Dummy>(
988 "octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
989 )
990 .map(|d| d.0)
991 .unwrap(),
992 @r#"
993 Repository(
994 RepositoryUses {
995 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89",
996 dependent: RepositoryUsesInner {
997 owner: "octo-org",
998 repo: "this-repo",
999 slug: "octo-org/this-repo",
1000 subpath: Some(
1001 ".github/workflows/workflow-1.yml",
1002 ),
1003 git_ref: "172239021f7ba04fe7327647b213799853a9eb89",
1004 },
1005 },
1006 )
1007 "#
1008 );
1009
1010 insta::assert_debug_snapshot!(
1011 serde_yaml::from_str::<Dummy>(
1012 "octo-org/this-repo/.github/workflows/workflow-1.yml@notahash"
1013 ).map(|d| d.0).unwrap(),
1014 @r#"
1015 Repository(
1016 RepositoryUses {
1017 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@notahash",
1018 dependent: RepositoryUsesInner {
1019 owner: "octo-org",
1020 repo: "this-repo",
1021 slug: "octo-org/this-repo",
1022 subpath: Some(
1023 ".github/workflows/workflow-1.yml",
1024 ),
1025 git_ref: "notahash",
1026 },
1027 },
1028 )
1029 "#
1030 );
1031
1032 insta::assert_debug_snapshot!(
1033 serde_yaml::from_str::<Dummy>(
1034 "octo-org/this-repo/.github/workflows/workflow-1.yml@abcd"
1035 ).map(|d| d.0).unwrap(),
1036 @r#"
1037 Repository(
1038 RepositoryUses {
1039 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@abcd",
1040 dependent: RepositoryUsesInner {
1041 owner: "octo-org",
1042 repo: "this-repo",
1043 slug: "octo-org/this-repo",
1044 subpath: Some(
1045 ".github/workflows/workflow-1.yml",
1046 ),
1047 git_ref: "abcd",
1048 },
1049 },
1050 )
1051 "#
1052 );
1053
1054 insta::assert_debug_snapshot!(
1056 serde_yaml::from_str::<Dummy>(
1057 "octo-org/this-repo/.github/workflows/workflow-1.yml"
1058 ).map(|d| d.0).unwrap_err(),
1059 @r#"Error("malformed `uses` ref: missing `@<ref>` in octo-org/this-repo/.github/workflows/workflow-1.yml")"#
1060 );
1061
1062 insta::assert_debug_snapshot!(
1064 serde_yaml::from_str::<Dummy>(
1065 "./.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
1066 ).map(|d| d.0).unwrap_err(),
1067 @r#"Error("local reusable workflow reference can't specify `@<ref>`")"#
1068 );
1069
1070 insta::assert_debug_snapshot!(
1072 serde_yaml::from_str::<Dummy>(
1073 ".github/workflows/workflow-1.yml"
1074 ).map(|d| d.0).unwrap_err(),
1075 @r#"Error("malformed `uses` ref: missing `@<ref>` in .github/workflows/workflow-1.yml")"#
1076 );
1077
1078 insta::assert_debug_snapshot!(
1080 serde_yaml::from_str::<Dummy>(
1081 "workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
1082 ).map(|d| d.0).unwrap_err(),
1083 @r#"Error("malformed `uses` ref: owner/repo slug is too short: workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89")"#
1084 );
1085 }
1086}