1use std::fmt::{self, Display};
4
5use indexmap::IndexMap;
6use self_cell::self_cell;
7use serde::{Deserialize, Deserializer, Serialize, de};
8
9pub mod expr;
10
11#[derive(Deserialize, Debug, PartialEq)]
13#[serde(rename_all = "kebab-case", untagged)]
14pub enum Permissions {
15 Base(BasePermission),
17 Explicit(IndexMap<String, Permission>),
22}
23
24impl Default for Permissions {
25 fn default() -> Self {
26 Self::Base(BasePermission::Default)
27 }
28}
29
30#[derive(Deserialize, Debug, Default, PartialEq)]
33#[serde(rename_all = "kebab-case")]
34pub enum BasePermission {
35 #[default]
37 Default,
38 ReadAll,
40 WriteAll,
42}
43
44#[derive(Deserialize, Debug, Default, PartialEq)]
46#[serde(rename_all = "kebab-case")]
47pub enum Permission {
48 Read,
50
51 Write,
53
54 #[default]
56 None,
57}
58
59pub type Env = IndexMap<String, EnvValue>;
61
62#[derive(Deserialize, Serialize, Debug, PartialEq)]
70#[serde(untagged)]
71pub enum EnvValue {
72 #[serde(deserialize_with = "null_to_default")]
74 String(String),
75 Number(f64),
76 Boolean(bool),
77}
78
79impl Display for EnvValue {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 match self {
82 Self::String(s) => write!(f, "{s}"),
83 Self::Number(n) => write!(f, "{n}"),
84 Self::Boolean(b) => write!(f, "{b}"),
85 }
86 }
87}
88
89impl EnvValue {
90 pub fn is_empty(&self) -> bool {
94 match self {
95 EnvValue::String(s) => s.is_empty(),
96 _ => false,
97 }
98 }
99
100 pub fn csharp_trueish(&self) -> bool {
107 match self {
108 EnvValue::Boolean(true) => true,
109 EnvValue::String(maybe) => maybe.trim().eq_ignore_ascii_case("true"),
110 _ => false,
111 }
112 }
113}
114
115#[derive(Deserialize, Debug, PartialEq)]
120#[serde(untagged)]
121enum SoV<T> {
122 One(T),
123 Many(Vec<T>),
124}
125
126impl<T> From<SoV<T>> for Vec<T> {
127 fn from(val: SoV<T>) -> Vec<T> {
128 match val {
129 SoV::One(v) => vec![v],
130 SoV::Many(vs) => vs,
131 }
132 }
133}
134
135pub(crate) fn scalar_or_vector<'de, D, T>(de: D) -> Result<Vec<T>, D::Error>
136where
137 D: Deserializer<'de>,
138 T: Deserialize<'de>,
139{
140 SoV::deserialize(de).map(Into::into)
141}
142
143#[derive(Deserialize, Debug, PartialEq)]
147#[serde(untagged)]
148enum BoS {
149 Bool(bool),
150 String(String),
151}
152
153impl From<BoS> for String {
154 fn from(value: BoS) -> Self {
155 match value {
156 BoS::Bool(b) => b.to_string(),
157 BoS::String(s) => s,
158 }
159 }
160}
161
162#[derive(Deserialize, Serialize, Debug, PartialEq)]
166#[serde(untagged)]
167pub enum If {
168 Bool(bool),
169 Expr(String),
172}
173
174pub(crate) fn bool_is_string<'de, D>(de: D) -> Result<String, D::Error>
175where
176 D: Deserializer<'de>,
177{
178 BoS::deserialize(de).map(Into::into)
179}
180
181fn null_to_default<'de, D, T>(de: D) -> Result<T, D::Error>
182where
183 D: Deserializer<'de>,
184 T: Default + Deserialize<'de>,
185{
186 let key = Option::<T>::deserialize(de)?;
187 Ok(key.unwrap_or_default())
188}
189
190#[derive(Debug, PartialEq)]
192pub struct UsesError(String);
193
194impl fmt::Display for UsesError {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 write!(f, "malformed `uses` ref: {}", self.0)
197 }
198}
199
200#[derive(Debug, PartialEq)]
201pub enum Uses {
202 Local(LocalUses),
204
205 Repository(RepositoryUses),
207
208 Docker(DockerUses),
210}
211
212impl Uses {
213 pub fn parse(uses: impl Into<String>) -> Result<Self, UsesError> {
215 let uses = uses.into();
216
217 if uses.starts_with("./") {
218 Ok(Self::Local(LocalUses::new(uses)))
219 } else if let Some(image) = uses.strip_prefix("docker://") {
220 Ok(Self::Docker(DockerUses::parse(image)))
221 } else {
222 RepositoryUses::parse(uses).map(Self::Repository)
223 }
224 }
225
226 pub fn raw(&self) -> &str {
228 match self {
229 Uses::Local(local) => &local.path,
230 Uses::Repository(repo) => repo.raw(),
231 Uses::Docker(docker) => docker.raw(),
232 }
233 }
234}
235
236#[derive(Debug, PartialEq)]
238#[non_exhaustive]
239pub struct LocalUses {
240 pub path: String,
241}
242
243impl LocalUses {
244 fn new(path: String) -> Self {
245 LocalUses { path }
246 }
247}
248
249#[derive(Debug, PartialEq)]
250struct RepositoryUsesInner<'a> {
251 owner: &'a str,
253 repo: &'a str,
255 slug: &'a str,
257 subpath: Option<&'a str>,
259 git_ref: &'a str,
261}
262
263impl<'a> RepositoryUsesInner<'a> {
264 fn from_str(uses: &'a str) -> Result<Self, UsesError> {
265 let (path, git_ref) = match uses.rsplit_once('@') {
268 Some((path, git_ref)) => (path, git_ref),
269 None => return Err(UsesError(format!("missing `@<ref>` in {uses}"))),
270 };
271
272 let mut components = path.splitn(3, '/');
273
274 if let Some(owner) = components.next()
275 && let Some(repo) = components.next()
276 {
277 let subpath = components.next();
278
279 let slug = if subpath.is_none() {
280 path
281 } else {
282 &path[..owner.len() + 1 + repo.len()]
283 };
284
285 Ok(RepositoryUsesInner {
286 owner,
287 repo,
288 slug,
289 subpath,
290 git_ref,
291 })
292 } else {
293 Err(UsesError(format!("owner/repo slug is too short: {uses}")))
294 }
295 }
296}
297
298self_cell!(
299 pub struct RepositoryUses {
301 owner: String,
302
303 #[covariant]
304 dependent: RepositoryUsesInner,
305 }
306
307 impl {Debug, PartialEq}
308);
309
310impl Display for RepositoryUses {
311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 write!(f, "{}", self.raw())
313 }
314}
315
316impl RepositoryUses {
317 pub fn parse(uses: impl Into<String>) -> Result<Self, UsesError> {
319 RepositoryUses::try_new(uses.into(), |s| {
320 let inner = RepositoryUsesInner::from_str(s)?;
321 Ok(inner)
322 })
323 }
324
325 pub fn raw(&self) -> &str {
327 self.borrow_owner()
328 }
329
330 pub fn owner(&self) -> &str {
332 self.borrow_dependent().owner
333 }
334
335 pub fn repo(&self) -> &str {
337 self.borrow_dependent().repo
338 }
339
340 pub fn slug(&self) -> &str {
342 self.borrow_dependent().slug
343 }
344
345 pub fn subpath(&self) -> Option<&str> {
347 self.borrow_dependent().subpath
348 }
349
350 pub fn git_ref(&self) -> &str {
352 self.borrow_dependent().git_ref
353 }
354}
355
356#[derive(Debug, PartialEq)]
357#[non_exhaustive]
358pub struct DockerUsesInner<'a> {
359 registry: Option<&'a str>,
361 image: &'a str,
363 tag: Option<&'a str>,
365 hash: Option<&'a str>,
367}
368
369impl<'a> DockerUsesInner<'a> {
370 fn is_registry(registry: &str) -> bool {
371 registry == "localhost" || registry.contains('.') || registry.contains(':')
373 }
374
375 fn from_str(uses: &'a str) -> Self {
376 let (registry, image) = match uses.split_once('/') {
377 Some((registry, image)) if Self::is_registry(registry) => (Some(registry), image),
378 _ => (None, uses),
379 };
380
381 if let Some(at_pos) = image.find('@') {
385 let (image, hash) = image.split_at(at_pos);
386
387 let hash = if hash.is_empty() {
388 None
389 } else {
390 Some(&hash[1..])
391 };
392
393 DockerUsesInner {
394 registry,
395 image,
396 tag: None,
397 hash,
398 }
399 } else {
400 let (image, tag) = match image.split_once(':') {
401 Some((image, "")) => (image, None),
402 Some((image, tag)) => (image, Some(tag)),
403 _ => (image, None),
404 };
405
406 DockerUsesInner {
407 registry,
408 image,
409 tag,
410 hash: None,
411 }
412 }
413 }
414}
415
416self_cell!(
417 pub struct DockerUses {
419 owner: String,
420
421 #[covariant]
422 dependent: DockerUsesInner,
423 }
424
425 impl {Debug, PartialEq}
426);
427
428impl DockerUses {
429 pub fn parse(uses: impl Into<String>) -> Self {
431 DockerUses::new(uses.into(), |s| DockerUsesInner::from_str(s))
432 }
433
434 pub fn raw(&self) -> &str {
436 self.borrow_owner()
437 }
438
439 pub fn registry(&self) -> Option<&str> {
441 self.borrow_dependent().registry
442 }
443
444 pub fn image(&self) -> &str {
446 self.borrow_dependent().image
447 }
448
449 pub fn tag(&self) -> Option<&str> {
451 self.borrow_dependent().tag
452 }
453
454 pub fn hash(&self) -> Option<&str> {
456 self.borrow_dependent().hash
457 }
458}
459
460impl<'de> Deserialize<'de> for DockerUses {
461 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
462 where
463 D: Deserializer<'de>,
464 {
465 let uses = <String>::deserialize(deserializer)?;
466 Ok(DockerUses::parse(uses))
467 }
468}
469
470pub(crate) fn custom_error<'de, D>(msg: impl Display) -> D::Error
476where
477 D: Deserializer<'de>,
478{
479 let msg = msg.to_string();
480 tracing::error!(msg);
481 de::Error::custom(msg)
482}
483
484pub(crate) fn step_uses<'de, D>(de: D) -> Result<Uses, D::Error>
486where
487 D: Deserializer<'de>,
488{
489 let uses = <String>::deserialize(de)?;
490 Uses::parse(uses).map_err(custom_error::<D>)
491}
492
493pub(crate) fn reusable_step_uses<'de, D>(de: D) -> Result<Uses, D::Error>
495where
496 D: Deserializer<'de>,
497{
498 let uses = step_uses(de)?;
499
500 match uses {
501 Uses::Repository(_) => Ok(uses),
502 Uses::Local(ref local) => {
503 if local.path.contains('@') {
508 Err(custom_error::<D>(
509 "local reusable workflow reference can't specify `@<ref>`",
510 ))
511 } else {
512 Ok(uses)
513 }
514 }
515 Uses::Docker(_) => Err(custom_error::<D>(
517 "docker action invalid in reusable workflow `uses`",
518 )),
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use indexmap::IndexMap;
525 use serde::Deserialize;
526
527 use crate::common::{BasePermission, Env, EnvValue, Permission};
528
529 use super::{Permissions, Uses, reusable_step_uses};
530
531 #[test]
532 fn test_permissions() {
533 assert_eq!(
534 serde_yaml::from_str::<Permissions>("read-all").unwrap(),
535 Permissions::Base(BasePermission::ReadAll)
536 );
537
538 let perm = "security-events: write";
539 assert_eq!(
540 serde_yaml::from_str::<Permissions>(perm).unwrap(),
541 Permissions::Explicit(IndexMap::from([(
542 "security-events".into(),
543 Permission::Write
544 )]))
545 );
546 }
547
548 #[test]
549 fn test_env_empty_value() {
550 let env = "foo:";
551 assert_eq!(
552 serde_yaml::from_str::<Env>(env).unwrap()["foo"],
553 EnvValue::String("".into())
554 );
555 }
556
557 #[test]
558 fn test_env_value_csharp_trueish() {
559 let vectors = [
560 (EnvValue::Boolean(true), true),
561 (EnvValue::Boolean(false), false),
562 (EnvValue::String("true".to_string()), true),
563 (EnvValue::String("TRUE".to_string()), true),
564 (EnvValue::String("TrUe".to_string()), true),
565 (EnvValue::String(" true ".to_string()), true),
566 (EnvValue::String(" \n\r\t True\n\n".to_string()), true),
567 (EnvValue::String("false".to_string()), false),
568 (EnvValue::String("1".to_string()), false),
569 (EnvValue::String("yes".to_string()), false),
570 (EnvValue::String("on".to_string()), false),
571 (EnvValue::String("random".to_string()), false),
572 (EnvValue::Number(1.0), false),
573 (EnvValue::Number(0.0), false),
574 (EnvValue::Number(666.0), false),
575 ];
576
577 for (val, expected) in vectors {
578 assert_eq!(val.csharp_trueish(), expected, "failed for {val:?}");
579 }
580 }
581
582 #[test]
583 fn test_uses_parses() {
584 insta::assert_debug_snapshot!(
586 Uses::parse("actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
587 @r#"
588 Repository(
589 RepositoryUses {
590 owner: "actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
591 dependent: RepositoryUsesInner {
592 owner: "actions",
593 repo: "checkout",
594 slug: "actions/checkout",
595 subpath: None,
596 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
597 },
598 },
599 )
600 "#,
601 );
602
603 insta::assert_debug_snapshot!(
605 Uses::parse("actions/aws/ec2@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
606 @r#"
607 Repository(
608 RepositoryUses {
609 owner: "actions/aws/ec2@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
610 dependent: RepositoryUsesInner {
611 owner: "actions",
612 repo: "aws",
613 slug: "actions/aws",
614 subpath: Some(
615 "ec2",
616 ),
617 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
618 },
619 },
620 )
621 "#
622 );
623
624 insta::assert_debug_snapshot!(
626 Uses::parse("example/foo/bar/baz/quux@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap(),
627 @r#"
628 Repository(
629 RepositoryUses {
630 owner: "example/foo/bar/baz/quux@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
631 dependent: RepositoryUsesInner {
632 owner: "example",
633 repo: "foo",
634 slug: "example/foo",
635 subpath: Some(
636 "bar/baz/quux",
637 ),
638 git_ref: "8f4b7f84864484a7bf31766abe9204da3cbe65b3",
639 },
640 },
641 )
642 "#
643 );
644
645 insta::assert_debug_snapshot!(
647 Uses::parse("actions/checkout@v4").unwrap(),
648 @r#"
649 Repository(
650 RepositoryUses {
651 owner: "actions/checkout@v4",
652 dependent: RepositoryUsesInner {
653 owner: "actions",
654 repo: "checkout",
655 slug: "actions/checkout",
656 subpath: None,
657 git_ref: "v4",
658 },
659 },
660 )
661 "#
662 );
663
664 insta::assert_debug_snapshot!(
665 Uses::parse("actions/checkout@abcd").unwrap(),
666 @r#"
667 Repository(
668 RepositoryUses {
669 owner: "actions/checkout@abcd",
670 dependent: RepositoryUsesInner {
671 owner: "actions",
672 repo: "checkout",
673 slug: "actions/checkout",
674 subpath: None,
675 git_ref: "abcd",
676 },
677 },
678 )
679 "#
680 );
681
682 insta::assert_debug_snapshot!(
684 Uses::parse("actions/checkout").unwrap_err(),
685 @r#"
686 UsesError(
687 "missing `@<ref>` in actions/checkout",
688 )
689 "#
690 );
691
692 insta::assert_debug_snapshot!(
694 Uses::parse("docker://alpine:3.8").unwrap(),
695 @r#"
696 Docker(
697 DockerUses {
698 owner: "alpine:3.8",
699 dependent: DockerUsesInner {
700 registry: None,
701 image: "alpine",
702 tag: Some(
703 "3.8",
704 ),
705 hash: None,
706 },
707 },
708 )
709 "#
710 );
711
712 insta::assert_debug_snapshot!(
714 Uses::parse("docker://localhost/alpine:3.8").unwrap(),
715 @r#"
716 Docker(
717 DockerUses {
718 owner: "localhost/alpine:3.8",
719 dependent: DockerUsesInner {
720 registry: Some(
721 "localhost",
722 ),
723 image: "alpine",
724 tag: Some(
725 "3.8",
726 ),
727 hash: None,
728 },
729 },
730 )
731 "#
732 );
733
734 insta::assert_debug_snapshot!(
736 Uses::parse("docker://localhost:1337/alpine:3.8").unwrap(),
737 @r#"
738 Docker(
739 DockerUses {
740 owner: "localhost:1337/alpine:3.8",
741 dependent: DockerUsesInner {
742 registry: Some(
743 "localhost:1337",
744 ),
745 image: "alpine",
746 tag: Some(
747 "3.8",
748 ),
749 hash: None,
750 },
751 },
752 )
753 "#
754 );
755
756 insta::assert_debug_snapshot!(
758 Uses::parse("docker://ghcr.io/foo/alpine:3.8").unwrap(),
759 @r#"
760 Docker(
761 DockerUses {
762 owner: "ghcr.io/foo/alpine:3.8",
763 dependent: DockerUsesInner {
764 registry: Some(
765 "ghcr.io",
766 ),
767 image: "foo/alpine",
768 tag: Some(
769 "3.8",
770 ),
771 hash: None,
772 },
773 },
774 )
775 "#
776 );
777
778 insta::assert_debug_snapshot!(
780 Uses::parse("docker://ghcr.io/foo/alpine").unwrap(),
781 @r#"
782 Docker(
783 DockerUses {
784 owner: "ghcr.io/foo/alpine",
785 dependent: DockerUsesInner {
786 registry: Some(
787 "ghcr.io",
788 ),
789 image: "foo/alpine",
790 tag: None,
791 hash: None,
792 },
793 },
794 )
795 "#
796 );
797
798 insta::assert_debug_snapshot!(
800 Uses::parse("docker://ghcr.io/foo/alpine:").unwrap(),
801 @r#"
802 Docker(
803 DockerUses {
804 owner: "ghcr.io/foo/alpine:",
805 dependent: DockerUsesInner {
806 registry: Some(
807 "ghcr.io",
808 ),
809 image: "foo/alpine",
810 tag: None,
811 hash: None,
812 },
813 },
814 )
815 "#
816 );
817
818 insta::assert_debug_snapshot!(
820 Uses::parse("docker://alpine").unwrap(),
821 @r#"
822 Docker(
823 DockerUses {
824 owner: "alpine",
825 dependent: DockerUsesInner {
826 registry: None,
827 image: "alpine",
828 tag: None,
829 hash: None,
830 },
831 },
832 )
833 "#
834 );
835
836 insta::assert_debug_snapshot!(
838 Uses::parse("docker://alpine@hash").unwrap(),
839 @r#"
840 Docker(
841 DockerUses {
842 owner: "alpine@hash",
843 dependent: DockerUsesInner {
844 registry: None,
845 image: "alpine",
846 tag: None,
847 hash: Some(
848 "hash",
849 ),
850 },
851 },
852 )
853 "#
854 );
855
856 insta::assert_debug_snapshot!(
858 Uses::parse("./.github/actions/hello-world-action@172239021f7ba04fe7327647b213799853a9eb89").unwrap(),
859 @r#"
860 Local(
861 LocalUses {
862 path: "./.github/actions/hello-world-action@172239021f7ba04fe7327647b213799853a9eb89",
863 },
864 )
865 "#
866 );
867
868 insta::assert_debug_snapshot!(
870 Uses::parse("./.github/actions/hello-world-action").unwrap(),
871 @r#"
872 Local(
873 LocalUses {
874 path: "./.github/actions/hello-world-action",
875 },
876 )
877 "#
878 );
879
880 insta::assert_debug_snapshot!(
882 Uses::parse("checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3").unwrap_err(),
883 @r#"
884 UsesError(
885 "owner/repo slug is too short: checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3",
886 )
887 "#
888 );
889 }
890
891 #[test]
892 fn test_uses_deser_reusable() {
893 #[derive(Deserialize)]
895 #[serde(transparent)]
896 struct Dummy(#[serde(deserialize_with = "reusable_step_uses")] Uses);
897
898 insta::assert_debug_snapshot!(
899 serde_yaml::from_str::<Dummy>(
900 "octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
901 )
902 .map(|d| d.0)
903 .unwrap(),
904 @r#"
905 Repository(
906 RepositoryUses {
907 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89",
908 dependent: RepositoryUsesInner {
909 owner: "octo-org",
910 repo: "this-repo",
911 slug: "octo-org/this-repo",
912 subpath: Some(
913 ".github/workflows/workflow-1.yml",
914 ),
915 git_ref: "172239021f7ba04fe7327647b213799853a9eb89",
916 },
917 },
918 )
919 "#
920 );
921
922 insta::assert_debug_snapshot!(
923 serde_yaml::from_str::<Dummy>(
924 "octo-org/this-repo/.github/workflows/workflow-1.yml@notahash"
925 ).map(|d| d.0).unwrap(),
926 @r#"
927 Repository(
928 RepositoryUses {
929 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@notahash",
930 dependent: RepositoryUsesInner {
931 owner: "octo-org",
932 repo: "this-repo",
933 slug: "octo-org/this-repo",
934 subpath: Some(
935 ".github/workflows/workflow-1.yml",
936 ),
937 git_ref: "notahash",
938 },
939 },
940 )
941 "#
942 );
943
944 insta::assert_debug_snapshot!(
945 serde_yaml::from_str::<Dummy>(
946 "octo-org/this-repo/.github/workflows/workflow-1.yml@abcd"
947 ).map(|d| d.0).unwrap(),
948 @r#"
949 Repository(
950 RepositoryUses {
951 owner: "octo-org/this-repo/.github/workflows/workflow-1.yml@abcd",
952 dependent: RepositoryUsesInner {
953 owner: "octo-org",
954 repo: "this-repo",
955 slug: "octo-org/this-repo",
956 subpath: Some(
957 ".github/workflows/workflow-1.yml",
958 ),
959 git_ref: "abcd",
960 },
961 },
962 )
963 "#
964 );
965
966 insta::assert_debug_snapshot!(
968 serde_yaml::from_str::<Dummy>(
969 "octo-org/this-repo/.github/workflows/workflow-1.yml"
970 ).map(|d| d.0).unwrap_err(),
971 @r#"Error("malformed `uses` ref: missing `@<ref>` in octo-org/this-repo/.github/workflows/workflow-1.yml")"#
972 );
973
974 insta::assert_debug_snapshot!(
976 serde_yaml::from_str::<Dummy>(
977 "./.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
978 ).map(|d| d.0).unwrap_err(),
979 @r#"Error("local reusable workflow reference can't specify `@<ref>`")"#
980 );
981
982 insta::assert_debug_snapshot!(
984 serde_yaml::from_str::<Dummy>(
985 ".github/workflows/workflow-1.yml"
986 ).map(|d| d.0).unwrap_err(),
987 @r#"Error("malformed `uses` ref: missing `@<ref>` in .github/workflows/workflow-1.yml")"#
988 );
989
990 insta::assert_debug_snapshot!(
992 serde_yaml::from_str::<Dummy>(
993 "workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89"
994 ).map(|d| d.0).unwrap_err(),
995 @r#"Error("malformed `uses` ref: owner/repo slug is too short: workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89")"#
996 );
997 }
998}