| 22 | 22 | | //! bundled as a Workers project's is, without a Wrangler config of its |
| 23 | 23 | | //! own. A framework set up to need a server fails before anything is |
| 24 | 24 | | //! installed, saying what to change. |
| 25 | + | //! - A container (deploy/container.rs): a project with a `Dockerfile` (or |
| 26 | + | //! `Containerfile`, or the path it chose) and no framework that builds a |
| 27 | + | //! static site or a Worker, a framework set up to need a server, or a |
| 28 | + | //! Node.js, Python, Go, Ruby or PHP server, for which g1t writes a |
| 29 | + | //! Dockerfile and shows it in the log. The image is built with Docker |
| 30 | + | //! and pushed to Cloudflare's registry with a push-only login the |
| 31 | + | //! deployments service gives this build alone, for minutes (the |
| 32 | + | //! `registry` step); the service runs it. |
| 25 | 33 | | //! - Anything else: a static site. Its `build` script runs, and the first |
| 26 | 34 | | //! of `dist`, `build`, `out`, `public`, `_site` or `.output/public` that |
| 27 | 35 | | //! exists is served, or the repository itself if it has an `index.html`. |
| 28 | 36 | | //! |
| 29 | | − | //! The project's own **Build command** and **Output directory** win over |
| 30 | | − | //! what is detected. |
| 37 | + | //! The project's own **Build as** (`BUILD_AS`: auto, static, workers or |
| 38 | + | //! docker), **Dockerfile** (`DOCKERFILE`), **Build command** and **Output |
| 39 | + | //! directory** win over what is detected. |
| 31 | 40 | | //! |
| 32 | 41 | | //! Configuration comes from the environment: |
| 33 | 42 | | //! |
| ⋯ |
| 63 | 72 | | use crate::checks::{redact, run_command}; |
| 64 | 73 | | use crate::{WORKDIR, auth_option, env, git}; |
| 65 | 74 | | |
| 75 | + | mod container; |
| 66 | 76 | | mod frameworks; |
| 67 | 77 | | |
| 78 | + | use container::BuildAs; |
| 68 | 79 | | use frameworks::{Adapter, Framework}; |
| 69 | 80 | | |
| 70 | 81 | | /// Where `wrangler deploy --dry-run` writes the bundle in a sandbox. |
| ⋯ |
| 525 | 536 | | assets_dir: Option<PathBuf>, |
| 526 | 537 | | warnings: Vec<String>, |
| 527 | 538 | | /// What kind of project it was found to be, for its settings page: |
| 528 | | − | /// `workers`, `static` (a build's output) or `html` (the files as they are). |
| 539 | + | /// `workers`, `static` (a build's output), `html` (the files as they |
| 540 | + | /// are) or `container`. |
| 529 | 541 | | detected: &'static str, |
| 530 | | − | /// The framework it was found to be built with, if any (`astro`…). |
| 542 | + | /// The framework it was found to be built with, if any (`astro`…), or |
| 543 | + | /// for a container what it was built from (`dockerfile`, `flask`…). |
| 531 | 544 | | framework: Option<&'static str>, |
| 545 | + | /// For a container: the image pushed, its digest, port and size. |
| 546 | + | container: Option<Value>, |
| 532 | 547 | | } |
| 533 | 548 | | |
| 534 | 549 | | /// What a project without a Workers config is: a site it built, or its own |
| ⋯ |
| 597 | 612 | | /// unless one was chosen or the framework says where it writes. |
| 598 | 613 | | #[derive(Debug, PartialEq, Eq)] |
| 599 | 614 | | pub(crate) struct Detected { |
| 600 | | − | /// `workers`, `static`, `html`, or `none` when there is nothing to serve. |
| 615 | + | /// `workers`, `static`, `html`, `container`, or `none` when there is |
| 616 | + | /// nothing to serve. |
| 601 | 617 | | pub kind: &'static str, |
| 602 | 618 | | /// The framework it is built with, if g1t knows it. |
| 603 | 619 | | pub framework: Option<Framework>, |
| ⋯ |
| 610 | 626 | | /// What runs before wrangler bundles a Workers project; for anything |
| 611 | 627 | | /// else, `build`. |
| 612 | 628 | | pre_build: Option<String>, |
| 629 | + | /// For a container: its Dockerfile, or the one g1t writes. |
| 630 | + | pub container: Option<ContainerBuild>, |
| 631 | + | /// What it would be served as without its Dockerfile, when it is a |
| 632 | + | /// container only for having one: built so while g1t.page runs no |
| 633 | + | /// containers (`not_enabled` from the registry step). |
| 634 | + | fallback: Option<Box<Detected>>, |
| 613 | 635 | | } |
| 614 | 636 | | |
| 637 | + | /// How a container is built. |
| 638 | + | #[derive(Debug, Clone, PartialEq, Eq)] |
| 639 | + | pub(crate) struct ContainerBuild { |
| 640 | + | /// The project's own Dockerfile, relative to the project; none when |
| 641 | + | /// g1t writes one. |
| 642 | + | pub dockerfile: Option<String>, |
| 643 | + | /// The Dockerfile g1t writes, for a server it recognises. |
| 644 | + | pub generated: Option<container::Server>, |
| 645 | + | /// What it listens on: its Dockerfile's `EXPOSE`, else 8080. `PORT` |
| 646 | + | /// is set to it. |
| 647 | + | pub port: u16, |
| 648 | + | } |
| 649 | + | |
| 650 | + | impl Detected { |
| 651 | + | fn nothing(framework: Option<Framework>, install: Option<String>, problem: String) -> Detected { |
| 652 | + | Detected { |
| 653 | + | kind: "none", |
| 654 | + | framework, |
| 655 | + | install, |
| 656 | + | build: None, |
| 657 | + | output_dir: None, |
| 658 | + | problem: Some(problem), |
| 659 | + | pre_build: None, |
| 660 | + | container: None, |
| 661 | + | fallback: None, |
| 662 | + | } |
| 663 | + | } |
| 664 | + | |
| 665 | + | /// The `framework` `g1t-runner detect` prints: the framework, else for |
| 666 | + | /// a container the server g1t wrote a Dockerfile for (`flask`…) or |
| 667 | + | /// `dockerfile`. |
| 668 | + | pub(crate) fn framework_id(&self) -> Option<&'static str> { |
| 669 | + | self.framework.as_ref().map(|f| f.id).or_else(|| { |
| 670 | + | self.container.as_ref().map(|c| c.generated.as_ref().map_or("dockerfile", |server| server.id)) |
| 671 | + | }) |
| 672 | + | } |
| 673 | + | } |
| 674 | + | |
| 675 | + | /// Why nothing could be found to serve, and what to add. |
| 676 | + | const NOTHING_TO_SERVE: &str = "found nothing to serve: no Workers config, no Dockerfile, no index.html, and none of dist, build, out, \ |
| 677 | + | public, _site or .output/public. Add a `Dockerfile` to deploy it as a container, or set **Build command** and \ |
| 678 | + | **Output directory** for a static site."; |
| 679 | + | |
| 680 | + | /// The project as a container: its own Dockerfile, else one g1t writes |
| 681 | + | /// for `framework` or for a server it recognises. None when it has neither. |
| 682 | + | fn as_container(dir: &Path, framework: Option<Framework>, own: Option<String>) -> Option<Detected> { |
| 683 | + | let build = match own { |
| 684 | + | Some(path) => { |
| 685 | + | let text = std::fs::read_to_string(dir.join(&path)).unwrap_or_default(); |
| 686 | + | ContainerBuild { port: container::exposed_port(&text).unwrap_or(container::DEFAULT_PORT), dockerfile: Some(path), generated: None } |
| 687 | + | } |
| 688 | + | None => { |
| 689 | + | let server = match &framework { |
| 690 | + | // One set up for Workers (OpenNext's adapter) is not run as a server. |
| 691 | + | Some(framework) if framework.workers => None, |
| 692 | + | Some(framework) => container::for_framework(dir, framework.id), |
| 693 | + | None => container::server(dir), |
| 694 | + | }?; |
| 695 | + | ContainerBuild { dockerfile: None, generated: Some(server), port: container::DEFAULT_PORT } |
| 696 | + | } |
| 697 | + | }; |
| 698 | + | let file = build.dockerfile.clone().unwrap_or_else(|| "Dockerfile (written by g1t)".to_owned()); |
| 699 | + | Some(Detected { |
| 700 | + | kind: "container", |
| 701 | + | framework, |
| 702 | + | // The image installs what it needs: nothing runs before its build. |
| 703 | + | install: None, |
| 704 | + | build: Some(format!("docker build -f {file} .")), |
| 705 | + | output_dir: None, |
| 706 | + | problem: None, |
| 707 | + | pre_build: None, |
| 708 | + | container: Some(build), |
| 709 | + | fallback: None, |
| 710 | + | }) |
| 711 | + | } |
| 712 | + | |
| 713 | + | #[cfg(test)] |
| 615 | 714 | | pub(crate) fn detect(dir: &Path, custom_build: Option<&str>, chosen_output: Option<&str>) -> Result<Detected> { |
| 715 | + | detect_as(dir, custom_build, chosen_output, BuildAs::Auto, None) |
| 716 | + | } |
| 717 | + | |
| 718 | + | /// What the project is, by its **Build as** setting (`build_as`) and its |
| 719 | + | /// Dockerfile's path (`dockerfile`, when it chose one): |
| 720 | + | /// |
| 721 | + | /// - **Docker**: a container, from its Dockerfile or one g1t writes. |
| 722 | + | /// - **Static site**: built and served as files, whatever else it has. |
| 723 | + | /// - **Workers**: its Wrangler config or a framework adapter's Worker. |
| 724 | + | /// - **Auto**: a Wrangler config is a Workers project; a framework that |
| 725 | + | /// builds a static site or a Worker is that, which costs nothing while |
| 726 | + | /// no one visits; a framework set up to need a server is a container |
| 727 | + | /// when it has a Dockerfile, or when g1t can write one; a project with |
| 728 | + | /// no framework g1t knows is a container when it has a Dockerfile or is |
| 729 | + | /// a server g1t recognises (a Node.js, Python, Go, Ruby or PHP app), |
| 730 | + | /// else a static site, else nothing, saying to add a Dockerfile. |
| 731 | + | pub(crate) fn detect_as( |
| 732 | + | dir: &Path, |
| 733 | + | custom_build: Option<&str>, |
| 734 | + | chosen_output: Option<&str>, |
| 735 | + | build_as: BuildAs, |
| 736 | + | dockerfile: Option<&str>, |
| 737 | + | ) -> Result<Detected> { |
| 738 | + | let own = container::own_dockerfile(dir, dockerfile)?; |
| 739 | + | match build_as { |
| 740 | + | BuildAs::Docker => { |
| 741 | + | let framework = frameworks::detect(dir, read_wrangler(dir)?.is_some()); |
| 742 | + | Ok(as_container(dir, framework.clone(), own).unwrap_or_else(|| { |
| 743 | + | Detected::nothing( |
| 744 | + | framework, |
| 745 | + | None, |
| 746 | + | "**Build as** is Docker, but this project has no Dockerfile and g1t doesn't recognise a server it can write \ |
| 747 | + | one for. Add a `Dockerfile` (or set its path under Settings, Deployments)." |
| 748 | + | .to_owned(), |
| 749 | + | ) |
| 750 | + | })) |
| 751 | + | } |
| 752 | + | BuildAs::Static => detect_site(dir, custom_build, chosen_output, true), |
| 753 | + | BuildAs::Workers => { |
| 754 | + | let site = detect_site(dir, custom_build, chosen_output, false)?; |
| 755 | + | if site.kind == "workers" { |
| 756 | + | return Ok(site); |
| 757 | + | } |
| 758 | + | Ok(Detected::nothing( |
| 759 | + | site.framework, |
| 760 | + | site.install, |
| 761 | + | "**Build as** is Workers, but this project has no Wrangler config (`wrangler.jsonc`, `wrangler.json` or \ |
| 762 | + | `wrangler.toml`) and no framework adapter that writes a Worker." |
| 763 | + | .to_owned(), |
| 764 | + | )) |
| 765 | + | } |
| 766 | + | BuildAs::Auto => { |
| 767 | + | let site = detect_site(dir, custom_build, chosen_output, false)?; |
| 768 | + | if site.kind == "workers" || (site.framework.is_some() && site.problem.is_none()) { |
| 769 | + | return Ok(site); |
| 770 | + | } |
| 771 | + | if let Some(problem) = site.problem.clone() { |
| 772 | + | // A framework set up to need a server. |
| 773 | + | let found = as_container(dir, site.framework.clone(), own); |
| 774 | + | return Ok(found.unwrap_or_else(|| { |
| 775 | + | Detected::nothing(site.framework, site.install, format!("{problem} Or add a `Dockerfile` to run it as a container.")) |
| 776 | + | })); |
| 777 | + | } |
| 778 | + | let has_own = own.is_some(); |
| 779 | + | if let Some(mut found) = as_container(dir, None, own) { |
| 780 | + | // A Dockerfile beside a site that could be served as it is. |
| 781 | + | if has_own && (site.kind == "static" || site.kind == "html") { |
| 782 | + | found.fallback = Some(Box::new(site)); |
| 783 | + | } |
| 784 | + | return Ok(found); |
| 785 | + | } |
| 786 | + | if site.kind == "none" { |
| 787 | + | return Ok(Detected::nothing(site.framework, site.install, NOTHING_TO_SERVE.to_owned())); |
| 788 | + | } |
| 789 | + | Ok(site) |
| 790 | + | } |
| 791 | + | } |
| 792 | + | } |
| 793 | + | |
| 794 | + | /// What the project is as a static site or Workers project: `as_static` |
| 795 | + | /// ignores its Wrangler config and any framework adapter, and builds it |
| 796 | + | /// even when its framework is set up to need a server. |
| 797 | + | fn detect_site(dir: &Path, custom_build: Option<&str>, chosen_output: Option<&str>, as_static: bool) -> Result<Detected> { |
| 616 | 798 | | let custom_build = custom_build.map(str::trim).filter(|c| !c.is_empty()); |
| 617 | 799 | | let chosen_output = chosen_output.map(|d| d.trim().trim_matches('/')).filter(|d| !d.is_empty()); |
| 618 | | − | let config = read_wrangler(dir)?; |
| 800 | + | let config = if as_static { None } else { read_wrangler(dir)? }; |
| 619 | 801 | | let framework = frameworks::detect(dir, config.is_some()); |
| 620 | 802 | | let installs: Vec<&str> = [install_command(dir), framework.as_ref().and_then(|f| f.install)].into_iter().flatten().collect(); |
| 621 | 803 | | let install = (!installs.is_empty()).then(|| installs.join(" && ")); |
| ⋯ |
| 638 | 820 | | output_dir: config.assets.and_then(|assets| assets.directory), |
| 639 | 821 | | problem: None, |
| 640 | 822 | | pre_build, |
| 823 | + | container: None, |
| 824 | + | fallback: None, |
| 641 | 825 | | }); |
| 642 | 826 | | } |
| 643 | 827 | | let build = custom_build |
| ⋯ |
| 645 | 829 | | .or(framework_build) |
| 646 | 830 | | .or_else(|| has_build_script(dir).then(|| "npm run build".to_owned())); |
| 647 | 831 | | // An adapter's Worker, unless the project chose a folder to serve. |
| 648 | | − | if chosen_output.is_none() && framework.as_ref().is_some_and(|f| f.adapter.is_some()) { |
| 832 | + | if !as_static && chosen_output.is_none() && framework.as_ref().is_some_and(|f| f.adapter.is_some()) { |
| 649 | 833 | | // Where its files are is known once it has built: the adapter's |
| 650 | 834 | | // own folder, or where the config it writes says. |
| 651 | 835 | | return Ok(Detected { |
| ⋯ |
| 656 | 840 | | pre_build: build.clone(), |
| 657 | 841 | | build, |
| 658 | 842 | | problem: None, |
| 843 | + | container: None, |
| 844 | + | fallback: None, |
| 659 | 845 | | }); |
| 660 | 846 | | } |
| 661 | | − | // A command or a folder of the project's own says it knows better. |
| 847 | + | // A command or a folder of the project's own says it knows better, as |
| 848 | + | // does **Build as** set to Static site. |
| 662 | 849 | | let problem = framework |
| 663 | 850 | | .as_ref() |
| 664 | 851 | | .and_then(|f| f.problem.clone()) |
| 665 | | − | .filter(|_| custom_build.is_none() && chosen_output.is_none()); |
| 666 | | − | if problem.is_some() { |
| 667 | | − | return Ok(Detected { kind: "none", framework, install, build: None, output_dir: None, problem, pre_build: None }); |
| 852 | + | .filter(|_| !as_static && custom_build.is_none() && chosen_output.is_none()); |
| 853 | + | if let Some(problem) = problem { |
| 854 | + | return Ok(Detected::nothing(framework, install, problem)); |
| 668 | 855 | | } |
| 669 | 856 | | let output_dir = chosen_output |
| 670 | 857 | | .map(str::to_owned) |
| ⋯ |
| 676 | 863 | | (None, None) => "none", |
| 677 | 864 | | _ => "static", |
| 678 | 865 | | }; |
| 679 | | − | Ok(Detected { kind, framework, install, pre_build: build.clone(), build, output_dir, problem: None }) |
| 866 | + | Ok(Detected { kind, framework, install, pre_build: build.clone(), build, output_dir, problem: None, container: None, fallback: None }) |
| 680 | 867 | | } |
| 681 | 868 | | |
| 682 | 869 | | /// The compatibility date a Worker gets when its config names none. |
| ⋯ |
| 723 | 910 | | bundle.with_file_name(format!("{}.wrangler.json", bundle.file_name().unwrap_or_default().to_string_lossy())) |
| 724 | 911 | | } |
| 725 | 912 | | |
| 726 | | − | fn build(log: &mut Log, secrets: &[String]) -> Result<Built> { |
| 913 | + | /// What a container build says when g1t.page runs no containers. |
| 914 | + | const CONTAINERS_OFF: &str = "This project deploys as a container, and Docker deployments are not switched on for g1t.page yet. \ |
| 915 | + | Set **Build as** to Static site or Workers under Settings, Deployments if it can be built as one."; |
| 916 | + | |
| 917 | + | /// Where a container's registry login is kept for the build: its own |
| 918 | + | /// folder, never the job's `~/.docker`. |
| 919 | + | fn docker_config_dir() -> PathBuf { |
| 920 | + | bundle_dir().with_file_name(format!("{}.docker", bundle_dir().file_name().unwrap_or_default().to_string_lossy())) |
| 921 | + | } |
| 922 | + | |
| 923 | + | /// Where the Dockerfile g1t writes for a project without one goes: beside |
| 924 | + | /// the bundle, never in the checkout. |
| 925 | + | fn generated_dockerfile_path() -> PathBuf { |
| 926 | + | bundle_dir().with_file_name(format!("{}.Dockerfile", bundle_dir().file_name().unwrap_or_default().to_string_lossy())) |
| 927 | + | } |
| 928 | + | |
| 929 | + | /// What the deployments service answers the `registry` step with: where |
| 930 | + | /// to push the app's image, with a login good for pushing only and only |
| 931 | + | /// for minutes, and the largest image the app's instance can run. |
| 932 | + | #[derive(Debug, Deserialize)] |
| 933 | + | struct RegistryGrant { |
| 934 | + | registry: String, |
| 935 | + | repository: String, |
| 936 | + | tag: String, |
| 937 | + | username: String, |
| 938 | + | password: String, |
| 939 | + | #[serde(alias = "maxImageBytes")] |
| 940 | + | max_image_bytes: u64, |
| 941 | + | #[serde(default, alias = "instanceType")] |
| 942 | + | instance_type: Option<String>, |
| 943 | + | } |
| 944 | + | |
| 945 | + | /// `name@sha256:…` for `repository` among an image's repository digests, |
| 946 | + | /// as `docker image inspect` lists them, one a line. |
| 947 | + | fn pushed_digest(repo_digests: &str, registry: &str, repository: &str) -> Option<String> { |
| 948 | + | let prefix = format!("{registry}/{repository}@sha256:"); |
| 949 | + | repo_digests |
| 950 | + | .lines() |
| 951 | + | .map(str::trim) |
| 952 | + | .find(|line| line.starts_with(&prefix) && line.len() == prefix.len() + 64) |
| 953 | + | .map(str::to_owned) |
| 954 | + | } |
| 955 | + | |
| 956 | + | /// Bytes, as a person reads them: `1.4 GB`. |
| 957 | + | fn size_text(bytes: u64) -> String { |
| 958 | + | format!("{:.1} GB", bytes as f64 / 1_000_000_000.0) |
| 959 | + | } |
| 960 | + | |
| 961 | + | /// Runs `docker` with `args`, signed in through `config`; its output, or |
| 962 | + | /// why it failed, redacted. |
| 963 | + | fn docker(args: &[&str], config: &Path, input: Option<&str>, secrets: &[String]) -> Result<String> { |
| 964 | + | use std::io::Write; |
| 965 | + | use std::process::{Command, Stdio}; |
| 966 | + | let mut command = Command::new("docker"); |
| 967 | + | command.args(args).env("DOCKER_CONFIG", config).stdout(Stdio::piped()).stderr(Stdio::piped()); |
| 968 | + | command.stdin(if input.is_some() { Stdio::piped() } else { Stdio::null() }); |
| 969 | + | let mut child = command.spawn().context("could not run docker")?; |
| 970 | + | if let (Some(input), Some(mut stdin)) = (input, child.stdin.take()) { |
| 971 | + | stdin.write_all(input.as_bytes())?; |
| 972 | + | } |
| 973 | + | let output = child.wait_with_output()?; |
| 974 | + | let text = redact(&format!("{}{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)), secrets); |
| 975 | + | if !output.status.success() { |
| 976 | + | bail!("`docker {}` failed: {}", args.first().copied().unwrap_or_default(), text.trim()); |
| 977 | + | } |
| 978 | + | Ok(text) |
| 979 | + | } |
| 980 | + | |
| 981 | + | /// Builds the project's container image with Docker and pushes it to |
| 982 | + | /// Cloudflare's registry, at the place and with the short-lived push-only |
| 983 | + | /// login the deployments service gives for this build (`registry`). The |
| 984 | + | /// sandbox never holds g1t's own Cloudflare token. None when g1t.page runs |
| 985 | + | /// no containers (the service answers `not_enabled`). |
| 986 | + | fn container_build(reporter: &Reporter, log: &mut Log, secrets: &[String], dir: &Path, plan: &Detected) -> Result<Option<Value>> { |
| 987 | + | let build = plan.container.as_ref().context("not a container")?; |
| 988 | + | // Asked first: a build that could not be pushed is not run. |
| 989 | + | let answer = reporter.send("registry", json!({}))?; |
| 990 | + | if answer["ok"] == false { |
| 991 | + | if answer["error"]["code"] == "not_enabled" { |
| 992 | + | return Ok(None); |
| 993 | + | } |
| 994 | + | bail!("{}", answer["error"]["message"].as_str().unwrap_or("g1t refused the image's upload")); |
| 995 | + | } |
| 996 | + | let grant: RegistryGrant = |
| 997 | + | serde_json::from_value(answer["value"].clone()).context("g1t's answer to the image's upload was not understood")?; |
| 998 | + | let mut secrets = secrets.to_vec(); |
| 999 | + | secrets.push(grant.password.clone()); |
| 1000 | + | let dockerfile = match (&build.dockerfile, &build.generated) { |
| 1001 | + | (Some(own), _) => { |
| 1002 | + | log.line(&format!("Building a container from {own}, listening on port {}.", build.port)); |
| 1003 | + | dir.join(own) |
| 1004 | + | } |
| 1005 | + | (None, Some(server)) => { |
| 1006 | + | // Shown whole, so it can be committed and changed. |
| 1007 | + | log.line(&format!("This {} project has no Dockerfile, so g1t wrote this one (commit it as `Dockerfile` to change it):", server.name)); |
| 1008 | + | for line in server.dockerfile.lines() { |
| 1009 | + | log.line(&format!(" {line}")); |
| 1010 | + | } |
| 1011 | + | let path = generated_dockerfile_path(); |
| 1012 | + | std::fs::write(&path, &server.dockerfile)?; |
| 1013 | + | // BuildKit reads `<Dockerfile>.dockerignore` beside it. |
| 1014 | + | std::fs::write(path.with_file_name(format!("{}.dockerignore", path.file_name().unwrap_or_default().to_string_lossy())), container::DOCKERIGNORE)?; |
| 1015 | + | path |
| 1016 | + | } |
| 1017 | + | (None, None) => bail!("nothing to build the container from"), |
| 1018 | + | }; |
| 1019 | + | if std::env::var("BUILD_COMMAND").is_ok_and(|c| !c.trim().is_empty()) { |
| 1020 | + | log.line("Build command is not used for a container: its Dockerfile builds it."); |
| 1021 | + | } |
| 1022 | + | // A sandbox's own Docker Engine, started for the build; in a workflow |
| 1023 | + | // step, the job's. |
| 1024 | + | if clones() { |
| 1025 | + | crate::docker::engine::enable(crate::docker::engine::Options::default()) |
| 1026 | + | .map_err(|problem| anyhow::anyhow!("Docker is not available in this sandbox: {problem}"))?; |
| 1027 | + | let started = crate::docker::engine::ensure_started(); |
| 1028 | + | for note in crate::docker::take_notes() { |
| 1029 | + | log.line(¬e); |
| 1030 | + | } |
| 1031 | + | started.map_err(|problem| anyhow::anyhow!("Docker could not start: {problem}"))?; |
| 1032 | + | } |
| 1033 | + | let config = docker_config_dir(); |
| 1034 | + | std::fs::create_dir_all(&config)?; |
| 1035 | + | let image = format!("{}/{}:{}", grant.registry, grant.repository, grant.tag); |
| 1036 | + | let quote = |path: &Path| format!("'{}'", path.display().to_string().replace('\'', r"'\''")); |
| 1037 | + | let built = step( |
| 1038 | + | log, |
| 1039 | + | &format!("docker build --platform linux/amd64 --pull -f {} -t {image} .", quote(&dockerfile)), |
| 1040 | + | &secrets, |
| 1041 | + | ); |
| 1042 | + | let pushed = built.and_then(|()| { |
| 1043 | + | let size: u64 = docker(&["image", "inspect", "--format", "{{.Size}}", &image], &config, None, &secrets)?.trim().parse().unwrap_or(0); |
| 1044 | + | log.line(&format!("The image is {}.", size_text(size))); |
| 1045 | + | if size > grant.max_image_bytes { |
| 1046 | + | bail!( |
| 1047 | + | "The image is {}, more than the {} an app of its size ({}) can hold. Make it smaller (a slim base image, a multi-stage build), or choose a larger instance under Settings, Deployments.", |
| 1048 | + | size_text(size), |
| 1049 | + | size_text(grant.max_image_bytes), |
| 1050 | + | grant.instance_type.as_deref().unwrap_or("its instance type"), |
| 1051 | + | ); |
| 1052 | + | } |
| 1053 | + | docker(&["login", &grant.registry, "--username", &grant.username, "--password-stdin"], &config, Some(&grant.password), &secrets)?; |
| 1054 | + | log.line(&format!("$ docker push {image}")); |
| 1055 | + | let output = docker(&["push", &image], &config, None, &secrets)?; |
| 1056 | + | log.line(output.trim_end()); |
| 1057 | + | let digests = docker(&["image", "inspect", "--format", "{{join .RepoDigests \"\\n\"}}", &image], &config, None, &secrets)?; |
| 1058 | + | let reference = pushed_digest(&digests, &grant.registry, &grant.repository).context("the pushed image's digest was not found")?; |
| 1059 | + | Ok(json!({ |
| 1060 | + | "image": reference, |
| 1061 | + | "port": build.port, |
| 1062 | + | "sizeBytes": size, |
| 1063 | + | "dockerfile": build.dockerfile, |
| 1064 | + | "generated": build.generated.as_ref().map(|server| server.id), |
| 1065 | + | })) |
| 1066 | + | }); |
| 1067 | + | let _ = std::fs::remove_dir_all(&config); |
| 1068 | + | let _ = std::fs::remove_file(generated_dockerfile_path()); |
| 1069 | + | pushed.map(Some) |
| 1070 | + | } |
| 1071 | + | |
| 1072 | + | fn build(reporter: &Reporter, log: &mut Log, secrets: &[String]) -> Result<Built> { |
| 727 | 1073 | | let project = project_dir(); |
| 728 | 1074 | | let dir = project.as_path(); |
| 729 | 1075 | | let custom_build = std::env::var("BUILD_COMMAND").ok(); |
| 730 | 1076 | | let chosen = std::env::var("OUTPUT_DIR").ok().filter(|d| !d.trim().is_empty()); |
| 731 | | − | let plan = detect(dir, custom_build.as_deref(), chosen.as_deref())?; |
| 732 | | − | let framework_id = plan.framework.as_ref().map(|f| f.id); |
| 1077 | + | let build_as = BuildAs::parse(&std::env::var("BUILD_AS").unwrap_or_default()); |
| 1078 | + | let dockerfile = std::env::var("DOCKERFILE").ok(); |
| 1079 | + | let mut plan = detect_as(dir, custom_build.as_deref(), chosen.as_deref(), build_as, dockerfile.as_deref())?; |
| 733 | 1080 | | if let Some(framework) = &plan.framework { |
| 734 | 1081 | | log.line(&format!("Detected {}: framework={}.", framework.name, framework.id)); |
| 735 | 1082 | | } |
| ⋯ |
| 737 | 1084 | | if let Some(problem) = &plan.problem { |
| 738 | 1085 | | bail!("{problem}"); |
| 739 | 1086 | | } |
| 1087 | + | if plan.container.is_some() { |
| 1088 | + | match container_build(reporter, log, secrets, dir, &plan)? { |
| 1089 | + | Some(image) => { |
| 1090 | + | return Ok(Built { |
| 1091 | + | worker: json!({}), |
| 1092 | + | assets_dir: None, |
| 1093 | + | warnings: Vec::new(), |
| 1094 | + | detected: "container", |
| 1095 | + | framework: plan.framework_id(), |
| 1096 | + | container: Some(image), |
| 1097 | + | }); |
| 1098 | + | } |
| 1099 | + | // g1t.page runs no containers yet: the site it is without its |
| 1100 | + | // Dockerfile, as it was built before. |
| 1101 | + | None => match plan.fallback.take() { |
| 1102 | + | Some(site) => { |
| 1103 | + | log.line(&format!("Building it as a {} site instead, as it is without its Dockerfile.", if site.kind == "html" { "plain HTML" } else { "static" })); |
| 1104 | + | plan = *site; |
| 1105 | + | } |
| 1106 | + | None => bail!("{CONTAINERS_OFF}"), |
| 1107 | + | }, |
| 1108 | + | } |
| 1109 | + | } |
| 1110 | + | let framework_id = plan.framework.as_ref().map(|f| f.id); |
| 740 | 1111 | | if let Some(install) = &plan.install { |
| 741 | 1112 | | step(log, install, secrets)?; |
| 742 | 1113 | | } |
| 743 | 1114 | | if let Some(command) = &plan.pre_build { |
| 744 | 1115 | | step(log, command, secrets)?; |
| 745 | 1116 | | } |
| 746 | | − | let own_config = read_wrangler(dir)?; |
| 1117 | + | // A static site (by **Build as**, or found to be one) never reads a |
| 1118 | + | // Workers config. |
| 1119 | + | let workers = plan.kind == "workers"; |
| 1120 | + | let own_config = if workers { read_wrangler(dir)? } else { None }; |
| 747 | 1121 | | // A framework built for Workers may write the config wrangler reads |
| 748 | 1122 | | // (`@cloudflare/vite-plugin` does, and Astro's adapter with it), with |
| 749 | 1123 | | // or without one of the project's own. |
| 750 | | − | let written = if own_config.is_some() || plan.framework.as_ref().is_some_and(|f| f.workers) { |
| 1124 | + | let written = if own_config.is_some() || (workers && plan.framework.as_ref().is_some_and(|f| f.workers)) { |
| 751 | 1125 | | redirected_config(dir)? |
| 752 | 1126 | | } else { |
| 753 | 1127 | | None |
| ⋯ |
| 755 | 1129 | | if let Some((config, base)) = written { |
| 756 | 1130 | | log.line(&format!("Using the Workers config the build wrote, in {}.", base.display())); |
| 757 | 1131 | | let (worker, assets_dir, warnings) = build_worker(log, secrets, config, &base, None)?; |
| 758 | | − | return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id }); |
| 1132 | + | return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id, container: None }); |
| 759 | 1133 | | } |
| 760 | 1134 | | if let Some(config) = own_config { |
| 761 | 1135 | | let (worker, assets_dir, warnings) = build_worker(log, secrets, config, dir, None)?; |
| 762 | | − | return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id }); |
| 1136 | + | return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id, container: None }); |
| 763 | 1137 | | } |
| 764 | | − | let adapter = plan.framework.as_ref().and_then(|f| f.adapter.as_ref()).filter(|_| chosen.is_none()); |
| 1138 | + | let adapter = plan.framework.as_ref().and_then(|f| f.adapter.as_ref()).filter(|_| workers && chosen.is_none()); |
| 765 | 1139 | | if let Some(adapter) = adapter |
| 766 | 1140 | | && let Some(entry) = adapter_entry(dir, adapter) |
| 767 | 1141 | | { |
| ⋯ |
| 792 | 1166 | | let built = build_worker(log, secrets, config, dir, Some(&file)); |
| 793 | 1167 | | let _ = std::fs::remove_file(&file); |
| 794 | 1168 | | let (worker, assets_dir, warnings) = built?; |
| 795 | | − | return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id }); |
| 1169 | + | return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id, container: None }); |
| 796 | 1170 | | } |
| 797 | 1171 | | if let (Some(adapter), Some(framework)) = (adapter, &plan.framework) |
| 798 | 1172 | | && framework.output.is_none() |
| ⋯ |
| 843 | 1217 | | assets_dir: Some(assets_dir), |
| 844 | 1218 | | warnings: Vec::new(), |
| 845 | 1219 | | framework: framework_id, |
| 1220 | + | container: None, |
| 846 | 1221 | | }) |
| 847 | 1222 | | } |
| 848 | 1223 | | |
| ⋯ |
| 929 | 1304 | | } |
| 930 | 1305 | | } |
| 931 | 1306 | | } |
| 932 | | − | let built = build(log, secrets)?; |
| 1307 | + | let built = build(reporter, log, secrets)?; |
| 933 | 1308 | | let mut finish = json!({ |
| 934 | 1309 | | "worker": built.worker, |
| 935 | 1310 | | "warnings": built.warnings, |
| 936 | 1311 | | "detected": built.detected, |
| 937 | | − | // Not stored by the deployments service yet, which ignores fields |
| 938 | | − | // it does not know; the build's log names it too. |
| 1312 | + | // What it is built with, shown beside `detected` on its settings. |
| 939 | 1313 | | "framework": built.framework, |
| 940 | 1314 | | }); |
| 1315 | + | if let Some(image) = built.container { |
| 1316 | + | finish["container"] = image; |
| 1317 | + | } |
| 941 | 1318 | | for warning in &built.warnings { |
| 942 | 1319 | | log.line(&format!("Warning: {warning}")); |
| 943 | 1320 | | } |
| ⋯ |
| 995 | 1372 | | |
| 996 | 1373 | | Reads DEPLOY_ID and DEPLOY_TOKEN (from POST /repos/{owner}/{repo}/deployments/builds), |
| 997 | 1374 | | G1T_API (https://api.g1t.sh), and optionally ROOT_DIR, BUILD_COMMAND, OUTPUT_DIR, |
| 998 | | − | BUILD_ENV and BUILD_SECRETS (JSON objects). The g1t/deploy action sets them all. |
| 1375 | + | BUILD_AS (auto, static, workers or docker), DOCKERFILE, and BUILD_ENV and |
| 1376 | + | BUILD_SECRETS (JSON objects). The g1t/deploy action sets them all. A container |
| 1377 | + | is built with the docker on PATH. |
| 999 | 1378 | | |
| 1000 | 1379 | | g1t-runner detect prints what the build would find, as key=value lines."; |
| 1001 | 1380 | | |
| ⋯ |
| 1030 | 1409 | | if step_log { |
| 1031 | 1410 | | let _ = std::fs::remove_dir_all(bundle_dir()); |
| 1032 | 1411 | | let _ = std::fs::remove_file(adapter_config_path()); |
| 1412 | + | let _ = std::fs::remove_dir_all(docker_config_dir()); |
| 1033 | 1413 | | } |
| 1034 | 1414 | | let (sent, failed) = match outcome { |
| 1035 | 1415 | | Ok(mut finish) => { |
| ⋯ |
| 1077 | 1457 | | "--root-dir" => root = rest.next().cloned().unwrap_or_default(), |
| 1078 | 1458 | | "--help" | "-h" => { |
| 1079 | 1459 | | println!( |
| 1080 | | − | "g1t-runner detect [--root-dir DIR]: prints kind, install, build, output-dir and framework (when one is found) as key=value lines." |
| 1460 | + | "g1t-runner detect [--root-dir DIR]: prints kind, install, build, output-dir and framework (when one is found), and for a container dockerfile and port, as key=value lines. Reads ROOT_DIR, BUILD_COMMAND, OUTPUT_DIR, BUILD_AS (auto, static, workers or docker) and DOCKERFILE as a build does." |
| 1081 | 1461 | | ); |
| 1082 | 1462 | | return 0; |
| 1083 | 1463 | | } |
| ⋯ |
| 1090 | 1470 | | let dir = project_dir_in(&checkout_dir(), &root); |
| 1091 | 1471 | | let custom = std::env::var("BUILD_COMMAND").ok(); |
| 1092 | 1472 | | let output = std::env::var("OUTPUT_DIR").ok(); |
| 1093 | | − | match detect(&dir, custom.as_deref(), output.as_deref()) { |
| 1473 | + | let build_as = BuildAs::parse(&std::env::var("BUILD_AS").unwrap_or_default()); |
| 1474 | + | let dockerfile = std::env::var("DOCKERFILE").ok(); |
| 1475 | + | match detect_as(&dir, custom.as_deref(), output.as_deref(), build_as, dockerfile.as_deref()) { |
| 1094 | 1476 | | Ok(found) => { |
| 1095 | 1477 | | print!("{}", output_lines(&found)); |
| 1096 | 1478 | | if let Some(problem) = &found.problem { |
| ⋯ |
| 1117 | 1499 | | one_line(found.build.as_deref()), |
| 1118 | 1500 | | one_line(found.output_dir.as_deref()), |
| 1119 | 1501 | | ); |
| 1120 | | − | if let Some(framework) = &found.framework { |
| 1121 | | − | lines.push_str(&format!("framework={}\n", framework.id)); |
| 1502 | + | if let Some(framework) = found.framework_id() { |
| 1503 | + | lines.push_str(&format!("framework={framework}\n")); |
| 1504 | + | } |
| 1505 | + | // A container's Dockerfile (empty when g1t writes one) and its port. |
| 1506 | + | if let Some(container) = &found.container { |
| 1507 | + | lines.push_str(&format!("dockerfile={}\nport={}\n", one_line(container.dockerfile.as_deref()), container.port)); |
| 1122 | 1508 | | } |
| 1123 | 1509 | | lines |
| 1124 | 1510 | | } |
| ⋯ |
| 1262 | 1648 | | output_dir: None, |
| 1263 | 1649 | | problem: None, |
| 1264 | 1650 | | pre_build: None, |
| 1651 | + | container: None, |
| 1652 | + | fallback: None, |
| 1265 | 1653 | | }; |
| 1266 | 1654 | | assert_eq!(output_lines(&found), "kind=static\ninstall=npm ci\nbuild=a b\noutput-dir=\n"); |
| 1267 | 1655 | | let vite = project("lines", &[("package.json", r#"{ "devDependencies": { "vite": "7" } }"#)]); |
| ⋯ |
| 1361 | 1749 | | Case { |
| 1362 | 1750 | | name: "astro-server", |
| 1363 | 1751 | | files: &[("package.json", r#"{ "dependencies": { "astro": "5", "@astrojs/node": "9" } }"#), ("astro.config.mjs", "export default { output: \"server\" }")], |
| 1364 | | − | kind: "none", |
| 1752 | + | kind: "container", |
| 1365 | 1753 | | framework: Some("astro"), |
| 1366 | | − | build: None, |
| 1367 | | − | problem: Some("`output: 'static'`"), |
| 1754 | + | build: Some(GENERATED), |
| 1368 | 1755 | | ..CASE |
| 1369 | 1756 | | }, |
| 1370 | 1757 | | Case { |
| ⋯ |
| 1392 | 1779 | | Case { |
| 1393 | 1780 | | name: "next-server", |
| 1394 | 1781 | | files: &[("package.json", r#"{ "scripts": { "build": "next build" }, "dependencies": { "next": "16" } }"#), ("next.config.mjs", "export default {}")], |
| 1395 | | − | kind: "none", |
| 1782 | + | kind: "container", |
| 1396 | 1783 | | framework: Some("nextjs"), |
| 1397 | | − | build: None, |
| 1398 | | − | problem: Some("`@opennextjs/cloudflare`"), |
| 1784 | + | build: Some(GENERATED), |
| 1399 | 1785 | | ..CASE |
| 1400 | 1786 | | }, |
| 1401 | 1787 | | Case { |
| ⋯ |
| 1460 | 1846 | | kind: "none", |
| 1461 | 1847 | | framework: Some("sveltekit"), |
| 1462 | 1848 | | build: None, |
| 1463 | | − | problem: Some("`@sveltejs/adapter-static`"), |
| 1849 | + | problem: Some("Or add a `Dockerfile`"), |
| 1464 | 1850 | | ..CASE |
| 1465 | 1851 | | }, |
| 1466 | 1852 | | Case { |
| ⋯ |
| 1492 | 1878 | | Case { |
| 1493 | 1879 | | name: "react-router-ssr", |
| 1494 | 1880 | | files: &[("package.json", r#"{ "devDependencies": { "@react-router/dev": "7" } }"#), ("react-router.config.ts", "export default { ssr: true }")], |
| 1495 | | − | kind: "none", |
| 1881 | + | kind: "container", |
| 1496 | 1882 | | framework: Some("react-router"), |
| 1497 | | − | build: None, |
| 1498 | | − | problem: Some("`ssr: false`"), |
| 1883 | + | build: Some(GENERATED), |
| 1499 | 1884 | | ..CASE |
| 1500 | 1885 | | }, |
| 1501 | 1886 | | Case { |
| ⋯ |
| 1627 | 2012 | | } |
| 1628 | 2013 | | } |
| 1629 | 2014 | | |
| 2015 | + | #[test] |
| 2016 | + | fn build_as_and_dockerfiles_decide_in_order() { |
| 2017 | + | let detect_with = |dir: &Path, build_as: BuildAs, dockerfile: Option<&str>| detect_as(dir, None, None, build_as, dockerfile).unwrap(); |
| 2018 | + | // A framework that builds a static site stays one beside a |
| 2019 | + | // Dockerfile: it costs nothing while no one visits. |
| 2020 | + | let vite = project("p-vite", &[("package.json", r#"{ "devDependencies": { "vite": "7" } }"#), ("Dockerfile", "FROM nginx\nEXPOSE 80\n")]); |
| 2021 | + | assert_eq!(detect_with(&vite, BuildAs::Auto, None).kind, "static"); |
| 2022 | + | // Unless the project says Docker. |
| 2023 | + | let found = detect_with(&vite, BuildAs::Docker, None); |
| 2024 | + | assert_eq!((found.kind, found.framework_id()), ("container", Some("vite"))); |
| 2025 | + | let container = found.container.unwrap(); |
| 2026 | + | assert_eq!((container.dockerfile.as_deref(), container.port), (Some("Dockerfile"), 80)); |
| 2027 | + | // A Wrangler config wins in Auto, and not when it says Static site. |
| 2028 | + | let workers = project("p-workers", &[("wrangler.jsonc", r#"{ "assets": { "directory": "public" } }"#), ("index.html", "")]); |
| 2029 | + | assert_eq!(detect_with(&workers, BuildAs::Auto, None).kind, "workers"); |
| 2030 | + | assert_eq!(detect_with(&workers, BuildAs::Static, None).kind, "html"); |
| 2031 | + | // Workers, without a config or adapter, says what is missing. |
| 2032 | + | let html = project("p-html", &[("index.html", "<h1>hi</h1>")]); |
| 2033 | + | let found = detect_with(&html, BuildAs::Workers, None); |
| 2034 | + | assert_eq!(found.kind, "none"); |
| 2035 | + | assert!(found.problem.unwrap().contains("no Wrangler config")); |
| 2036 | + | // No framework and a Dockerfile: a container, built as the site it |
| 2037 | + | // is without one while g1t.page runs no containers. |
| 2038 | + | let both = project("p-both", &[("index.html", ""), ("Containerfile", "FROM nginx")]); |
| 2039 | + | let found = detect_with(&both, BuildAs::Auto, None); |
| 2040 | + | assert_eq!((found.kind, found.framework_id()), ("container", Some("dockerfile"))); |
| 2041 | + | assert_eq!(found.container.as_ref().unwrap().port, 8080); |
| 2042 | + | assert_eq!(found.fallback.as_ref().map(|site| site.kind), Some("html")); |
| 2043 | + | // A Dockerfile at a path of the project's choosing. |
| 2044 | + | let chosen = project("p-chosen", &[("ops/web.Dockerfile", "FROM node\nEXPOSE 3000")]); |
| 2045 | + | assert_eq!(detect_with(&chosen, BuildAs::Auto, None).kind, "none"); |
| 2046 | + | let found = detect_with(&chosen, BuildAs::Auto, Some("ops/web.Dockerfile")); |
| 2047 | + | assert_eq!((found.kind, found.container.unwrap().port), ("container", 3000)); |
| 2048 | + | assert!(detect_as(&chosen, None, None, BuildAs::Auto, Some("nope")).is_err()); |
| 2049 | + | // A server g1t recognises gets a Dockerfile written for it. |
| 2050 | + | let express = project("p-express", &[("package.json", r#"{ "scripts": { "start": "node index.js" }, "dependencies": { "express": "5" } }"#)]); |
| 2051 | + | let found = detect_with(&express, BuildAs::Auto, None); |
| 2052 | + | assert_eq!((found.kind, found.framework_id(), found.install.as_deref()), ("container", Some("node"), None)); |
| 2053 | + | assert!(found.fallback.is_none()); |
| 2054 | + | assert!(output_lines(&found).ends_with("framework=node\ndockerfile=\nport=8080\n")); |
| 2055 | + | // Nothing at all says to add a Dockerfile. |
| 2056 | + | let nothing = project("p-nothing", &[("README.md", "")]); |
| 2057 | + | let found = detect_with(¬hing, BuildAs::Auto, None); |
| 2058 | + | assert_eq!(found.kind, "none"); |
| 2059 | + | assert!(found.problem.as_deref().unwrap().starts_with("found nothing to serve")); |
| 2060 | + | assert!(found.problem.unwrap().contains("Add a `Dockerfile`")); |
| 2061 | + | let found = detect_with(¬hing, BuildAs::Docker, None); |
| 2062 | + | assert!(found.problem.unwrap().contains("**Build as** is Docker")); |
| 2063 | + | // A framework set up for a server, with a Dockerfile of its own. |
| 2064 | + | let next = project("p-next", &[("package.json", r#"{ "dependencies": { "next": "16" } }"#), ("Dockerfile", "FROM node")]); |
| 2065 | + | let found = detect_with(&next, BuildAs::Auto, None); |
| 2066 | + | assert_eq!((found.kind, found.framework_id()), ("container", Some("nextjs"))); |
| 2067 | + | assert_eq!(found.container.unwrap().dockerfile.as_deref(), Some("Dockerfile")); |
| 2068 | + | // OpenNext's adapter means Workers: it is not run as a server. |
| 2069 | + | let opennext = project("p-opennext", &[("package.json", r#"{ "dependencies": { "next": "16", "@opennextjs/cloudflare": "1" } }"#)]); |
| 2070 | + | assert_eq!(detect_with(&opennext, BuildAs::Auto, None).kind, "none"); |
| 2071 | + | for dir in [vite, workers, html, both, chosen, express, nothing, next, opennext] { |
| 2072 | + | let _ = std::fs::remove_dir_all(dir); |
| 2073 | + | } |
| 2074 | + | } |
| 2075 | + | |
| 2076 | + | #[test] |
| 2077 | + | fn the_pushed_digest_is_the_one_in_the_apps_repository() { |
| 2078 | + | let digest = "a".repeat(64); |
| 2079 | + | let lines = format!("docker.io/x@sha256:{digest}\nregistry.cloudflare.com/acc/other@sha256:{digest}\nregistry.cloudflare.com/acc/g1t-web@sha256:{digest}\n"); |
| 2080 | + | assert_eq!( |
| 2081 | + | pushed_digest(&lines, "registry.cloudflare.com", "acc/g1t-web"), |
| 2082 | + | Some(format!("registry.cloudflare.com/acc/g1t-web@sha256:{digest}")) |
| 2083 | + | ); |
| 2084 | + | assert_eq!(pushed_digest("registry.cloudflare.com/acc/g1t-web@sha256:short", "registry.cloudflare.com", "acc/g1t-web"), None); |
| 2085 | + | let grant: RegistryGrant = serde_json::from_value(json!({ |
| 2086 | + | "registry": "registry.cloudflare.com", "repository": "acc/g1t-web", "tag": "dpl_1", |
| 2087 | + | "username": "u", "password": "p", "max_image_bytes": 2_000_000_000u64, "instance_type": "lite" |
| 2088 | + | })) |
| 2089 | + | .unwrap(); |
| 2090 | + | assert_eq!((grant.max_image_bytes, grant.instance_type.as_deref()), (2_000_000_000, Some("lite"))); |
| 2091 | + | assert_eq!(size_text(1_450_000_000), "1.4 GB"); |
| 2092 | + | } |
| 2093 | + | |
| 2094 | + | /// What a container g1t writes the Dockerfile for is built with. |
| 2095 | + | const GENERATED: &str = "docker build -f Dockerfile (written by g1t) ."; |
| 2096 | + | |
| 1630 | 2097 | | fn frameworks_hugo() -> &'static str { |
| 1631 | 2098 | | "if command -v hugo >/dev/null 2>&1; then hugo --minify; else npx --yes hugo-extended@0.166.0 --minify; fi" |
| 1632 | 2099 | | } |