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