Repository: axodotdev/cargo-dist
Pull request #378 (2023-08-29 00:58:46)

ci++

mistydemeo: > I broke the publish-homebrew-formula task
> it's expecting steps.plan.outputs.releases which doesn't exist anymore
> should be changed to some use of fromJson(needs.plan.outputs.val)

Fixed.

> I haven't tested that publish-release still works (moderately confident it works)

It took a few tweaks to get this in good shape, but it's now working, including both artifacts and Homebrew. https://github.com/mistydemeo/cargodisttest/releases/tag/v0.1.31

> The rebase was hastily done, it's very possible I accidentally dropped some new improvements to the current github_ci.yml so we should both do a detailed test/review of this to be sure

I believe this is fine now.
mistydemeo: > What security measures if any exist to prevent a user from editing the workflow in their PR to remove this checks that this is a PR?

It's not perfect, but a maintainer would have to grant permission to the PR author to let it run.

Patch:
diff --git a/cargo-dist-schema/src/lib.rs b/cargo-dist-schema/src/lib.rs
index c6181691..2e8970c3 100644
--- a/cargo-dist-schema/src/lib.rs
+++ b/cargo-dist-schema/src/lib.rs
@@ -65,6 +65,79 @@ pub struct DistManifest {
     #[serde(default)]
     #[serde(skip_serializing_if = "BTreeMap::is_empty")]
     pub artifacts: BTreeMap<ArtifactId, Artifact>,
+    /// ci backend info
+    #[serde(default)]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ci: Option<CiInfo>,
+}
+
+/// CI backend info
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct CiInfo {
+    /// GitHub CI backend
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub github: Option<GithubCiInfo>,
+}
+
+/// Github CI backend
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct GithubCiInfo {
+    /// Github CI Matrix for upload-artifacts
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub artifacts_matrix: Option<GithubMatrix>,
+
+    /// What kind of job to run on pull request
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pr_run_mode: Option<PrRunMode>,
+}
+
+/// Github CI Matrix
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct GithubMatrix {
+    /// define each task manually rather than doing cross-product stuff
+    #[serde(default)]
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    pub include: Vec<GithubMatrixEntry>,
+}
+
+/// Entry for a github matrix
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct GithubMatrixEntry {
+    /// Github Runner to user
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub runner: Option<String>,
+    /// Expression to execute to install cargo-dist
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub install_dist: Option<String>,
+    /// Arguments to pass to cargo-dist
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub dist_args: Option<String>,
+}
+
+/// Type of job to run on pull request
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
+#[non_exhaustive]
+pub enum PrRunMode {
+    /// Do not run on pull requests at all
+    #[serde(rename = "skip")]
+    Skip,
+    /// Only run the plan step
+    #[serde(rename = "plan")]
+    Plan,
+    /// Build and upload artifacts
+    #[default]
+    #[serde(rename = "upload")]
+    Upload,
+}
+
+impl std::fmt::Display for PrRunMode {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            PrRunMode::Skip => write!(f, "skip"),
+            PrRunMode::Plan => write!(f, "plan"),
+            PrRunMode::Upload => write!(f, "upload"),
+        }
+    }
 }
 
 /// Info about the system/toolchain used to build this announcement.
@@ -274,6 +347,7 @@ impl DistManifest {
             system_info: None,
             releases,
             artifacts,
+            ci: None,
         }
     }
 
diff --git a/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap b/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
index 3ce907f4..8c8f2f23 100644
--- a/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
+++ b/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
@@ -48,6 +48,17 @@ expression: json_schema
         "$ref": "#/definitions/Artifact"
       }
     },
+    "ci": {
+      "description": "ci backend info",
+      "anyOf": [
+        {
+          "$ref": "#/definitions/CiInfo"
+        },
+        {
+          "type": "null"
+        }
+      ]
+    },
     "dist_version": {
       "description": "The version of cargo-dist that generated this",
       "type": [
@@ -311,6 +322,117 @@ expression: json_schema
         }
       }
     },
+    "CiInfo": {
+      "description": "CI backend info",
+      "type": "object",
+      "properties": {
+        "github": {
+          "description": "GitHub CI backend",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/GithubCiInfo"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        }
+      }
+    },
+    "GithubCiInfo": {
+      "description": "Github CI backend",
+      "type": "object",
+      "properties": {
+        "artifacts_matrix": {
+          "description": "Github CI Matrix for upload-artifacts",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/GithubMatrix"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "pr_run_mode": {
+          "description": "What kind of job to run on pull request",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/PrRunMode"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        }
+      }
+    },
+    "GithubMatrix": {
+      "description": "Github CI Matrix",
+      "type": "object",
+      "properties": {
+        "include": {
+          "description": "define each task manually rather than doing cross-product stuff",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/GithubMatrixEntry"
+          }
+        }
+      }
+    },
+    "GithubMatrixEntry": {
+      "description": "Entry for a github matrix",
+      "type": "object",
+      "properties": {
+        "dist_args": {
+          "description": "Arguments to pass to cargo-dist",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "install_dist": {
+          "description": "Expression to execute to install cargo-dist",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "runner": {
+          "description": "Github Runner to user",
+          "type": [
+            "string",
+            "null"
+          ]
+        }
+      }
+    },
+    "PrRunMode": {
+      "description": "Type of job to run on pull request",
+      "oneOf": [
+        {
+          "description": "Do not run on pull requests at all",
+          "type": "string",
+          "enum": [
+            "skip"
+          ]
+        },
+        {
+          "description": "Only run the plan step",
+          "type": "string",
+          "enum": [
+            "plan"
+          ]
+        },
+        {
+          "description": "Build and upload artifacts",
+          "type": "string",
+          "enum": [
+            "upload"
+          ]
+        }
+      ]
+    },
     "Release": {
       "description": "A Release of an Application",
       "type": "object",
diff --git a/cargo-dist/src/backend/ci/github.rs b/cargo-dist/src/backend/ci/github.rs
index d323f46e..61ff1771 100644
--- a/cargo-dist/src/backend/ci/github.rs
+++ b/cargo-dist/src/backend/ci/github.rs
@@ -5,6 +5,7 @@
 // FIXME(#283): migrate this to minijinja (steal logic from oranda to load a whole dir)
 
 use axoasset::LocalAsset;
+use cargo_dist_schema::{GithubMatrix, GithubMatrixEntry};
 use serde::Serialize;
 use tracing::warn;
 
@@ -12,124 +13,132 @@ use crate::{
     backend::templates::TEMPLATE_CI_GITHUB, DistGraph, SortedMap, SortedSet, TargetTriple,
 };
 
+/// Info about running cargo-dist in Github CI
 #[derive(Debug, Serialize)]
-struct CiInfo {
-    rust_version: Option<String>,
-    install_dist_sh: String,
-    install_dist_ps1: String,
-    fail_fast: bool,
-    create_release: bool,
-    local_tasks: Vec<CiTask>,
-    global_task: Option<CiTask>,
-    tap: Option<String>,
-    publish_jobs: Vec<String>,
+pub struct GithubCiInfo {
+    /// Version of rust toolchain to install (deprecated)
+    pub rust_version: Option<String>,
+    /// expression to use for installing cargo-dist via shell script
+    pub install_dist_sh: String,
+    /// expression to use for installing cargo-dist via powershell script
+    pub install_dist_ps1: String,
+    /// Whether to fail-fast
+    pub fail_fast: bool,
+    /// Matrix for upload-local-artifacts
+    pub artifacts_matrix: cargo_dist_schema::GithubMatrix,
+    /// What kind of job to run on pull request
+    pub pr_run_mode: cargo_dist_schema::PrRunMode,
+    /// global task
+    pub global_task: Option<GithubMatrixEntry>,
+    /// homebrew tap
+    pub tap: Option<String>,
+    /// publish jobs
+    pub publish_jobs: Vec<String>,
+    /// whether to create the release or assume an existing one
+    pub create_release: bool,
 }
 
-#[derive(Debug, Serialize)]
-struct CiTask {
-    runner: String,
-    dist_args: String,
-    install_dist: String,
-}
+impl GithubCiInfo {
+    /// Compute the Github CI stuff
+    pub fn new(dist: &DistGraph) -> GithubCiInfo {
+        // Legacy deprecated support
+        let rust_version = dist.desired_rust_toolchain.clone();
+
+        // If they don't specify a cargo-dist version, use this one
+        let self_dist_version = super::SELF_DIST_VERSION.parse().unwrap();
+        let dist_version = dist
+            .desired_cargo_dist_version
+            .as_ref()
+            .unwrap_or(&self_dist_version);
+        let fail_fast = dist.fail_fast;
+        let create_release = dist.create_release;
+
+        // Figure out what builds we need to do
+        let mut needs_global_build = false;
+        let mut local_targets = SortedSet::new();
+        for release in &dist.releases {
+            if !release.global_artifacts.is_empty() {
+                needs_global_build = true;
+            }
+            local_targets.extend(release.targets.iter());
+        }
 
-/// Generate CI for Github
-///
-/// This actually creates a file and writes to disk!
-pub fn generate_github_ci(dist: &DistGraph) -> Result<(), miette::Report> {
-    const GITHUB_CI_DIR: &str = ".github/workflows/";
-    const GITHUB_CI_FILE: &str = "release.yml";
-
-    // FIXME: should we try to avoid clobbering old files..?
-    let ci_dir = dist.workspace_dir.join(GITHUB_CI_DIR);
-    let ci_file = ci_dir.join(GITHUB_CI_FILE);
-    let ci_info = compute_ci_info(dist);
-
-    let rendered = dist
-        .templates
-        .render_file_to_clean_string(TEMPLATE_CI_GITHUB, &ci_info)?;
-    LocalAsset::write_new_all(&rendered, &ci_file)?;
-    eprintln!("generated Github CI to {}", ci_file);
-
-    Ok(())
-}
+        // Get the platform-specific installation methods
+        let install_dist_sh = super::install_dist_sh_for_version(dist_version);
+        let install_dist_ps1 = super::install_dist_ps1_for_version(dist_version);
+
+        // Build up the task matrix for building Artifacts
+        let mut tasks = vec![];
+
+        // If we have Global Artifacts, we need one task for that. If we've done a Good Job
+        // then these artifacts should be possible to build on *any* platform. Linux is usually
+        // fast/cheap, so that's a reasonable choice.s
+        let global_task = if needs_global_build {
+            Some(GithubMatrixEntry {
+                runner: Some(GITHUB_LINUX_RUNNER.into()),
+                dist_args: Some("--artifacts=global".into()),
+                install_dist: Some(install_dist_sh.clone()),
+            })
+        } else {
+            None
+        };
+
+        let pr_run_mode = dist.pr_run_mode.clone();
+
+        let tap = dist.tap.clone();
+        let publish_jobs = dist.publish_jobs.iter().map(|j| j.to_string()).collect();
+
+        // Figure out what Local Artifact tasks we need
+        let local_runs = if dist.merge_tasks {
+            distribute_targets_to_runners_merged(local_targets)
+        } else {
+            distribute_targets_to_runners_split(local_targets)
+        };
+        for (runner, targets) in local_runs {
+            use std::fmt::Write;
+            let install_dist =
+                install_dist_for_github_runner(runner, &install_dist_sh, &install_dist_ps1);
+            let mut dist_args = String::from("--artifacts=local");
+            for target in targets {
+                write!(dist_args, " --target={target}").unwrap();
+            }
+            tasks.push(GithubMatrixEntry {
+                runner: Some(runner.to_owned()),
+                dist_args: Some(dist_args),
+                install_dist: Some(install_dist.to_owned()),
+            });
+        }
 
-/// Write the Github CI to something
-fn compute_ci_info(dist: &DistGraph) -> CiInfo {
-    // Legacy deprecated support for
-    let rust_version = dist.desired_rust_toolchain.clone();
-
-    // If they don't specify a cargo-dist version, use this one
-    let self_dist_version = super::SELF_DIST_VERSION.parse().unwrap();
-    let dist_version = dist
-        .desired_cargo_dist_version
-        .as_ref()
-        .unwrap_or(&self_dist_version);
-    let fail_fast = dist.fail_fast;
-    let create_release = dist.create_release;
-
-    // Figure out what builds we need to do
-    let mut needs_global_build = false;
-    let mut local_targets = SortedSet::new();
-    for release in &dist.releases {
-        if !release.global_artifacts.is_empty() {
-            needs_global_build = true;
+        GithubCiInfo {
+            rust_version,
+            install_dist_sh,
+            install_dist_ps1,
+            fail_fast,
+            tap,
+            publish_jobs,
+            artifacts_matrix: GithubMatrix { include: tasks },
+            pr_run_mode,
+            global_task,
+            create_release,
         }
-        local_targets.extend(release.targets.iter());
     }
 
-    // Get the platform-specific installation methods
-    let install_dist_sh = super::install_dist_sh_for_version(dist_version);
-    let install_dist_ps1 = super::install_dist_ps1_for_version(dist_version);
-
-    // Build up the task matrix for building Artifacts
-    let mut local_tasks = vec![];
-
-    // If we have Global Artifacts, we need one task for that. If we've done a Good Job
-    // then these artifacts should be possible to build on *any* platform. Linux is usually
-    // fast/cheap, so that's a reasonable choice.s
-    let global_task = if needs_global_build {
-        Some(CiTask {
-            runner: GITHUB_LINUX_RUNNER.into(),
-            dist_args: "--artifacts=global".into(),
-            install_dist: install_dist_sh.clone(),
-        })
-    } else {
-        None
-    };
-    let tap = dist.tap.clone();
-    let publish_jobs = dist.publish_jobs.iter().map(|j| j.to_string()).collect();
+    /// Write release.yml to disk
+    pub fn write_to_disk(&self, dist: &DistGraph) -> Result<(), miette::Report> {
+        const GITHUB_CI_DIR: &str = ".github/workflows/";
+        const GITHUB_CI_FILE: &str = "release.yml";
 
-    // Figure out what Local Artifact tasks we need
-    let local_runs = if dist.merge_tasks {
-        distribute_targets_to_runners_merged(local_targets)
-    } else {
-        distribute_targets_to_runners_split(local_targets)
-    };
-    for (runner, targets) in local_runs {
-        use std::fmt::Write;
-        let install_dist =
-            install_dist_for_github_runner(runner, &install_dist_sh, &install_dist_ps1);
-        let mut dist_args = String::from("--artifacts=local");
-        for target in targets {
-            write!(dist_args, " --target={target}").unwrap();
-        }
-        local_tasks.push(CiTask {
-            runner: runner.to_owned(),
-            dist_args,
-            install_dist: install_dist.to_owned(),
-        });
-    }
+        // FIXME: should we try to avoid clobbering old files..?
+        let ci_dir = dist.workspace_dir.join(GITHUB_CI_DIR);
+        let ci_file = ci_dir.join(GITHUB_CI_FILE);
+
+        let rendered = dist
+            .templates
+            .render_file_to_clean_string(TEMPLATE_CI_GITHUB, self)?;
+        LocalAsset::write_new_all(&rendered, &ci_file)?;
+        eprintln!("generated Github CI to {}", ci_file);
 
-    CiInfo {
-        rust_version,
-        install_dist_sh,
-        install_dist_ps1,
-        fail_fast,
-        create_release,
-        local_tasks,
-        global_task,
-        tap,
-        publish_jobs,
+        Ok(())
     }
 }
 
diff --git a/cargo-dist/src/backend/ci/mod.rs b/cargo-dist/src/backend/ci/mod.rs
index 38b0788c..3b6024d3 100644
--- a/cargo-dist/src/backend/ci/mod.rs
+++ b/cargo-dist/src/backend/ci/mod.rs
@@ -2,12 +2,21 @@
 
 use semver::Version;
 
+use self::github::GithubCiInfo;
+
 pub mod github;
 
 /// The current version of cargo-dist
 const SELF_DIST_VERSION: &str = env!("CARGO_PKG_VERSION");
 const BASE_DIST_FETCH_URL: &str = "https://github.com/axodotdev/cargo-dist/releases/download";
 
+/// Info about all the enabled CI backends
+#[derive(Debug, Default)]
+pub struct CiInfo {
+    /// Github CI
+    pub github: Option<GithubCiInfo>,
+}
+
 /// Get the command to invoke to install cargo-dist via sh script
 fn install_dist_sh_for_version(version: &Version) -> String {
     if let Some(git) = install_dist_git(version) {
diff --git a/cargo-dist/src/config.rs b/cargo-dist/src/config.rs
index 7edf6081..5d168e25 100644
--- a/cargo-dist/src/config.rs
+++ b/cargo-dist/src/config.rs
@@ -55,6 +55,11 @@ pub struct DistMetadata {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub ci: Option<Vec<CiStyle>>,
 
+    /// TODO
+    #[serde(rename = "pr-run-mode")]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pr_run_mode: Option<cargo_dist_schema::PrRunMode>,
+
     /// The full set of installers you would like to produce
     ///
     /// When generating full task graphs (such as CI scripts) we will try to generate these.
@@ -269,6 +274,7 @@ impl DistMetadata {
             all_features: _,
             publish_jobs: _,
             create_release: _,
+            pr_run_mode: _,
         } = self;
         if let Some(include) = include {
             for include in include {
@@ -307,6 +313,7 @@ impl DistMetadata {
             all_features,
             publish_jobs,
             create_release,
+            pr_run_mode: _,
         } = self;
 
         // Check for global settings on local packages
diff --git a/cargo-dist/src/init.rs b/cargo-dist/src/init.rs
index 2d1401c2..3a51e93f 100644
--- a/cargo-dist/src/init.rs
+++ b/cargo-dist/src/init.rs
@@ -3,6 +3,7 @@ use std::ops::Not;
 use axoproject::errors::AxoprojectError;
 use axoproject::WorkspaceInfo;
 use camino::Utf8PathBuf;
+use cargo_dist_schema::PrRunMode;
 use semver::Version;
 use serde::Deserialize;
 
@@ -166,7 +167,7 @@ fn get_new_dist_metadata(
     args: &InitArgs,
     workspace_info: &WorkspaceInfo,
 ) -> DistResult<DistMetadata> {
-    use dialoguer::{Confirm, Input, MultiSelect};
+    use dialoguer::{Confirm, Input, MultiSelect, Select};
     // Setup [workspace.metadata.dist]
     let has_config = workspace_info
         .cargo_metadata_table
@@ -205,6 +206,7 @@ fn get_new_dist_metadata(
             all_features: None,
             publish_jobs: None,
             create_release: None,
+            pr_run_mode: None,
         }
     };
 
@@ -382,6 +384,30 @@ fn get_new_dist_metadata(
         }
     }
 
+    if has_github_ci {
+        let prompt = r#"enable Github CI checks on pull requests?
+    This will run most of your release process in order to allow you to
+    test it regularly. "Upload" will build and upload the release artifacts,
+    while "plan" will only plan out the release without actually
+    performing it."#;
+        let items = vec!["upload", "plan", "skip"];
+        let selection = Select::with_theme(&theme)
+            .with_prompt(prompt)
+            .items(&items)
+            .default(0)
+            .interact()?;
+        eprintln!();
+
+        let result = match items[selection] {
+            "upload" => PrRunMode::Upload,
+            "plan" => PrRunMode::Plan,
+            "skip" => PrRunMode::Skip,
+            _ => PrRunMode::default(),
+        };
+
+        meta.pr_run_mode = Some(result);
+    }
+
     // Enable installer backends (if they have a CI backend that can provide URLs)
     // In the future, "vendored" installers like MSIs could be enabled in this situation!
     let has_ci = meta.ci.as_ref().map(|ci| !ci.is_empty()).unwrap_or(false);
@@ -634,6 +660,7 @@ fn update_toml_metadata(
         default_features,
         publish_jobs,
         create_release,
+        pr_run_mode,
     } = &meta;
 
     apply_optional_value(
@@ -790,6 +817,13 @@ fn update_toml_metadata(
         publish_jobs.as_ref(),
     );
 
+    apply_optional_value(
+        table,
+        "pr-run-mode",
+        "# Publish jobs to run in CI\n",
+        pr_run_mode.as_ref().map(|m| m.to_string()),
+    );
+
     // Finalize the table
     table
         .decor_mut()
diff --git a/cargo-dist/src/lib.rs b/cargo-dist/src/lib.rs
index 05c6e6d0..ab625d53 100644
--- a/cargo-dist/src/lib.rs
+++ b/cargo-dist/src/lib.rs
@@ -17,12 +17,13 @@ use std::{
 
 use axoasset::LocalAsset;
 use backend::{
+    ci::CiInfo,
     installer::{self, homebrew::HomebrewInstallerInfo, npm::NpmInstallerInfo, InstallerImpl},
     templates::{TemplateEntry, TEMPLATE_INSTALLER_NPM},
 };
 use camino::{Utf8Path, Utf8PathBuf};
 use cargo_dist_schema::{Asset, AssetKind, DistManifest, ExecutableAsset};
-use config::{ChecksumStyle, CiStyle, CompressionImpl, Config, ZipStyle};
+use config::{ChecksumStyle, CompressionImpl, Config, ZipStyle};
 use semver::Version;
 use tracing::{info, warn};
 
@@ -119,7 +120,14 @@ fn build_manifest(cfg: &Config, dist: &DistGraph) -> DistManifest {
     }
 
     let mut manifest = DistManifest::new(releases, all_artifacts);
+
+    // build metadata
     manifest.dist_version = Some(env!("CARGO_PKG_VERSION").to_owned());
+    manifest.system_info = Some(cargo_dist_schema::SystemInfo {
+        cargo_version_line: dist.tools.cargo.version_line.clone(),
+    });
+
+    // announcement metadata
     manifest.announcement_tag = dist.announcement_tag.clone();
     manifest.announcement_is_prerelease = dist.announcement_is_prerelease;
     manifest.announcement_title = dist.announcement_title.clone();
@@ -128,6 +136,18 @@ fn build_manifest(cfg: &Config, dist: &DistGraph) -> DistManifest {
     manifest.system_info = Some(cargo_dist_schema::SystemInfo {
         cargo_version_line: dist.tools.cargo.version_line.clone(),
     });
+
+    // ci metadata
+    if !dist.ci_style.is_empty() {
+        let CiInfo { github } = &dist.ci;
+        let github = github.as_ref().map(|info| cargo_dist_schema::GithubCiInfo {
+            artifacts_matrix: Some(info.artifacts_matrix.clone()),
+            pr_run_mode: Some(info.pr_run_mode.clone()),
+        });
+
+        manifest.ci = Some(cargo_dist_schema::CiInfo { github });
+    }
+
     manifest
 }
 
@@ -573,10 +593,10 @@ pub fn do_generate_ci(cfg: &Config, _args: &GenerateCiArgs) -> Result<()> {
         ));
     }
 
-    for style in &dist.ci_style {
-        match style {
-            CiStyle::Github => backend::ci::github::generate_github_ci(&dist)?,
-        }
+    // If you add a CI backend, call its write_to_disk here
+    let CiInfo { github } = &dist.ci;
+    if let Some(github) = github {
+        github.write_to_disk(&dist)?;
     }
     Ok(())
 }
diff --git a/cargo-dist/src/tasks.rs b/cargo-dist/src/tasks.rs
index f0b4272f..e97b4385 100644
--- a/cargo-dist/src/tasks.rs
+++ b/cargo-dist/src/tasks.rs
@@ -14,7 +14,7 @@
 //!   2. add installers, each one decides if it's global or local
 //! 7. compute actual BuildSteps from the current graph (a Binary will only induce an actual `cargo build`
 //!    here if one of the Artifacts that was added requires outputs from it!)
-//! 8. (NOT YET IMPLEMENTED) generate release/announcement notes
+//! 8. generate release/announcement notes
 //!
 //! During step 6 a lot of extra magic happens:
 //!
@@ -60,6 +60,8 @@ use miette::{miette, Context, IntoDiagnostic};
 use semver::Version;
 use tracing::{info, warn};
 
+use crate::backend::ci::github::GithubCiInfo;
+use crate::backend::ci::CiInfo;
 use crate::{
     backend::{
         installer::{
@@ -159,6 +161,8 @@ pub struct DistGraph {
     pub desired_rust_toolchain: Option<String>,
     /// Styles of CI we want to support
     pub ci_style: Vec<CiStyle>,
+    /// TODO
+    pub pr_run_mode: cargo_dist_schema::PrRunMode,
     /// The git tag used for the announcement (e.g. v1.0.0)
     ///
     /// This is important for certain URLs like Github Releases
@@ -184,6 +188,8 @@ pub struct DistGraph {
     pub variants: Vec<ReleaseVariant>,
     /// Logical releases that artifacts are grouped under
     pub releases: Vec<Release>,
+    /// Info about CI backends
+    pub ci: CiInfo,
     /// List of publish jobs to run
     pub publish_jobs: Vec<PublishStyle>,
     /// A GitHub repo to publish the Homebrew formula to
@@ -633,6 +639,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             default_features: no_default_features,
             all_features,
             create_release,
+            pr_run_mode: _,
         } = &workspace_metadata;
 
         let desired_cargo_dist_version = cargo_dist_version.clone();
@@ -707,6 +714,8 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                 binaries: vec![],
                 variants: vec![],
                 releases: vec![],
+                ci: CiInfo::default(),
+                pr_run_mode: workspace_metadata.pr_run_mode.clone().unwrap_or_default(),
                 tap: workspace_metadata.tap.clone(),
                 publish_jobs,
             },
@@ -1857,6 +1866,16 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
         self.inner.announcement_github_body = Some(gh_body);
     }
 
+    fn compute_ci(&mut self) {
+        for ci in &self.inner.ci_style {
+            match ci {
+                CiStyle::Github => {
+                    self.inner.ci.github = Some(GithubCiInfo::new(&self.inner));
+                }
+            }
+        }
+    }
+
     fn workspace(&self) -> &'pkg_graph WorkspaceInfo {
         self.workspace
     }
@@ -2058,6 +2077,8 @@ pub fn gather_work(cfg: &Config) -> Result<DistGraph> {
     // Finally compute all the build steps!
     graph.compute_build_steps();
 
+    graph.compute_ci();
+
     Ok(graph.inner)
 }
 
diff --git a/cargo-dist/templates/ci/github_ci.yml.j2 b/cargo-dist/templates/ci/github_ci.yml.j2
index d9f3916f..711c8434 100644
--- a/cargo-dist/templates/ci/github_ci.yml.j2
+++ b/cargo-dist/templates/ci/github_ci.yml.j2
@@ -4,21 +4,13 @@
 # CI that:
 #
 # * checks for a Git Tag that looks like a release
-{{%- if create_release %}}
-# * creates a draft Github Release™ and fills in its text
-{{%- endif %}}
 # * builds artifacts with cargo-dist (executable-zips, installers, hashes)
-# * uploads those artifacts to the Github Release™
-# * undrafts the Github Release™ on success
+# * uploads those artifacts to temporary workflow zip
+# * on success, uploads the artifacts to a Github Release™
 {{%- if create_release %}}
 #
-# Note that the Github Release™ will be created before the artifacts,
-# so there will be a few minutes where the release has no artifacts
-# and then they will slowly trickle in, possibly failing. To make
-# this more pleasant we mark the release as a "draft" until all
-# artifacts have been successfully uploaded. This allows you to
-# choose what to do with partial successes and avoids spamming
-# anyone with notifications before the release is actually ready.
+# Note that the Github Release™ will be created with a generated
+# title/body based on your changelogs.
 {{%- else %}}
 #
 # Note that a Github Release™ with this tag is assumed to exist as a draft
@@ -54,17 +46,19 @@ on:
   push:
     tags:
       - '**[0-9]+.[0-9]+.[0-9]+*'
+  {{%- if pr_run_mode != "skip" %}}
+  pull_request:
+  {{%- endif %}}
 
 jobs:
   # Run 'cargo dist plan' to determine what tasks we need to do
-  {{%- if create_release %}}
-  # and create a draft github release with the computed title/body
-  {{%- endif %}}
   plan:
     runs-on: ubuntu-latest
     outputs:
-      has-releases: ${{ steps.plan.outputs.has-releases }}
-      releases: ${{ steps.plan.outputs.releases }}
+      val: ${{ steps.plan.outputs.manifest }}
+      tag: ${{ !github.event.pull_request && github.ref_name || '' }}
+      tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
+      publishing: ${{ !github.event.pull_request }}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
@@ -79,48 +73,35 @@ jobs:
         run: {{{ install_dist_sh }}}
       - id: plan
         run: |
-          cargo dist plan --tag=${{ github.ref_name }} --output-format=json > dist-manifest.json
-          echo "dist plan ran successfully"
+          cargo dist plan ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} --output-format=json > dist-manifest.json
+          echo "cargo dist plan ran successfully"
           cat dist-manifest.json
-
-      {{%- if create_release %}}
-
-          # Create the Github Release™ based on what cargo-dist thinks it should be
-          ANNOUNCEMENT_TITLE=$(jq --raw-output ".announcement_title" dist-manifest.json)
-          IS_PRERELEASE=$(jq --raw-output ".announcement_is_prerelease" dist-manifest.json)
-          jq --raw-output ".announcement_github_body" dist-manifest.json > new_dist_announcement.md
-          gh release create ${{ github.ref_name }} --draft --prerelease="$IS_PRERELEASE" --title="$ANNOUNCEMENT_TITLE" --notes-file=new_dist_announcement.md
-          echo "created announcement!"
-      {{%- else %}}
-
-          # We're assuming a draft Github Release™ with the desired title/tag/body already exists
-      {{%- endif %}}
-
-          # Upload the manifest to the Github Release™
-          gh release upload ${{ github.ref_name }} dist-manifest.json
-          echo "uploaded manifest!"
-
-          # Disable all the upload-artifacts tasks if we have no actual releases
-          HAS_RELEASES=$(jq --raw-output ".releases != null" dist-manifest.json)
-          echo "has-releases=$HAS_RELEASES" >> "$GITHUB_OUTPUT"
-          echo "releases=$(jq --compact-output ".releases" dist-manifest.json)" >> "$GITHUB_OUTPUT"
+          echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
+      - name: "Upload dist-manifest.json"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: dist-manifest.json
 
   # Build and packages all the platform-specific things
   upload-local-artifacts:
     # Let the initial task tell us to not run (currently very blunt)
     needs: plan
-    if: ${{ needs.plan.outputs.has-releases == 'true' }}
+    if: ${{ fromJson(needs.plan.outputs.val).releases != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
     strategy:
       fail-fast: {{{ fail_fast }}}
-      matrix:
-        # For these target platforms
-        include:
-        {{%- for task in local_tasks %}}
-        - os: {{{ task.runner }}}
-          dist-args: {{{ task.dist_args }}}
-          install-dist: {{{ task.install_dist }}}
-        {{%- endfor %}}
-    runs-on: ${{ matrix.os }}
+      # Target platforms/runners are computed by cargo-dist in create-release.
+      # Each member of the matrix has the following arguments:
+      #
+      # - runner: the github runner
+      # - dist-args: cli flags to pass to cargo dist
+      # - install-dist: expression to run to install cargo-dist on the runner
+      #
+      # Typically there will be:
+      # - 1 "global" task that builds universal installers
+      # - N "local" tasks that build each platform's binaries and platform-specific installers
+      matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
+    runs-on: ${{ matrix.runner }}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
@@ -131,31 +112,35 @@ jobs:
       - name: Install Rust
         run: rustup update {{{ rust_version }}} --no-self-update && rustup default {{{ rust_version }}}
       {{%- endif %}}
+      - uses: swatinem/rust-cache@v2
       - name: Install cargo-dist
-        run: ${{ matrix.install-dist }}
-      - name: Run cargo-dist
-        # This logic is a bit janky because it's trying to be a polyglot between
-        # powershell and bash since this will run on windows, macos, and linux!
-        # The two platforms don't agree on how to talk about env vars but they
-        # do agree on 'cat' and '$()' so we use that to marshal values between commands.
+        run: ${{ matrix.install_dist }}
+      - id: cargo-dist
+        # We force bash here just because github makes it really hard to get values up
+        # to "real" actions without writing to env-vars, and writing to env-vars has
+        # inconsistent syntax between shell and powershell. cargo-dist and jq work fine
+        # in powershell.
+        shell: bash
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build --tag=${{ github.ref_name }} --output-format=json ${{ matrix.dist-args }} > dist-manifest.json
-          echo "dist ran successfully"
-          cat dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          echo "cargo dist ran successfully"
 
           # Parse out what we just built and upload it to the Github Release™
-          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt
-          echo "uploading..."
-          cat uploads.txt
-          gh release upload ${{ github.ref_name }} $(cat uploads.txt)
-          echo "uploaded!"
+          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
+          echo "EOF" >> "$GITHUB_OUTPUT"
+      - name: "Upload artifacts"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: ${{ steps.cargo-dist.outputs.paths }}
 
 {{%- if global_task %}}
 
   # Build and package all the platform-agnostic(ish) things
   upload-global-artifacts:
-    needs: upload-local-artifacts
+    needs: [plan, upload-local-artifacts]
     runs-on: {{{ global_task.runner }}}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -171,29 +156,49 @@ jobs:
         run: {{{ global_task.install_dist }}}
       # Get all the local artifacts for the global tasks to use (for e.g. checksums)
       - name: Fetch local artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: target/distrib/
+      - id: cargo-dist
+        shell: bash
         run: |
-          gh release download ${{ github.ref_name }} --dir target/distrib/
-      - name: Run cargo-dist
-        run: |
-          cargo dist build --tag=${{ github.ref_name }} --output-format=json {{{ global_task.dist_args }}} > dist-manifest.json
-          echo "dist ran successfully"
-          cat dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json {{{ global_task.dist_args }}} > dist-manifest.json
+          echo "cargo dist ran successfully"
 
           # Parse out what we just built and upload it to the Github Release™
-          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt
-          echo "uploading..."
-          cat uploads.txt
-          gh release upload ${{ github.ref_name }} $(cat uploads.txt)
-          echo "uploaded!"
+          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
+          echo "EOF" >> "$GITHUB_OUTPUT"
+      - name: "Upload artifacts"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: ${{ steps.cargo-dist.outputs.paths }}
+{{%- endif %}}
+
+  should-publish:
+    needs:
+      - plan
+      - upload-local-artifacts
+    {{%- if global_task %}}
+      - upload-global-artifacts
+    {{%- endif %}}
+    if: ${{ needs.plan.outputs.publishing == 'true' }}
+    runs-on: ubuntu-latest
+    steps:
+      - name: print tag
+        run: echo "ok we're publishing!"
+
 
-  {{%- if 'homebrew' in publish_jobs and tap %}}
+{{%- if 'homebrew' in publish_jobs and tap %}}
 
-  upload-homebrew-formula:
-    needs: [plan, upload-global-artifacts]
+  publish-homebrew-formula:
+    needs: [plan, should-publish]
     runs-on: {{{ global_task.runner }}}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      RELEASES: ${{ needs.plan.outputs.releases }}
+      PLAN: ${{ needs.plan.outputs.val }}
       GITHUB_USER: "axo bot"
       GITHUB_EMAIL: "admin+bot@axo.dev"
     steps:
@@ -203,14 +208,16 @@ jobs:
           token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
       # So we have access to the formula
       - name: Fetch local artifacts
-        run: |
-          gh release download ${{ github.ref_name }} --dir Formula --repo ${GITHUB_REPOSITORY} --clobber
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: Formula/
       - name: Commit formula files
         run: |
           git config --global user.name "${GITHUB_USER}"
           git config --global user.email "${GITHUB_EMAIL}"
 
-          for release in $(echo "$RELEASES" | jq --compact-output '.[]'); do
+          for release in $(echo "$PLAN" | jq --compact-output '.releases[]'); do
             name=$(echo "$release" | jq .app_name --raw-output)
             version=$(echo "$release" | jq .app_version --raw-output)
 
@@ -219,15 +226,11 @@ jobs:
           done
           git push
 
-  {{%- endif %}}
-
 {{%- endif %}}
 
-  # Mark the Github Release™ as a non-draft now that everything has succeeded!
+  # Create a Github Release with all the results once everything is done,
   publish-release:
-    # Only run after all the other tasks, but it's ok if upload-artifacts was skipped
-    needs: [plan, upload-local-artifacts {{%- if global_task %}}, upload-global-artifacts{{% endif %}}]
-    if: ${{ always() && needs.plan.result == 'success' && (needs.upload-local-artifacts.result == 'skipped' || needs.upload-local-artifacts.result == 'success') {{%- if global_task %}} && (needs.upload-global-artifacts.result == 'skipped' || needs.upload-global-artifacts.result == 'success'){{% endif %}} }}
+    needs: [plan, should-publish]
     runs-on: ubuntu-latest
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -235,6 +238,23 @@ jobs:
       - uses: actions/checkout@v3
         with:
           submodules: recursive
-      - name: mark release as non-draft
-        run: |
-          gh release edit ${{ github.ref_name }} --draft=false
+      - name: "Download artifacts"
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: artifacts
+      - name: Create Release
+        uses: ncipollo/release-action@v1
+        with:
+          tag: ${{ needs.plan.outputs.tag }}
+        {{%- if create_release %}}
+          name: ${{ fromJson(needs.plan.outputs.val).announcement_title }}
+          body: ${{ fromJson(needs.plan.outputs.val).announcement_github_body }}
+        {{%- else %}}
+          allowUpdates: true
+          updateOnlyUnreleased: true
+          omitBodyDuringUpdate: true
+          omitNameDuringUpdate: true
+        {{%- endif %}}
+          prerelease: ${{ fromJson(needs.plan.outputs.val).announcement_is_prerelease }}
+          artifacts: "artifacts/*"


Test patch:
diff --git a/cargo-dist/tests/snapshots/akaikatana_basic.snap b/cargo-dist/tests/snapshots/akaikatana_basic.snap
index 700dcc18..6d0cd702 100644
--- a/cargo-dist/tests/snapshots/akaikatana_basic.snap
+++ b/cargo-dist/tests/snapshots/akaikatana_basic.snap
@@ -1235,6 +1235,35 @@ Install-Binary "$Args"
       "install_hint": "brew install mistydemeo/homebrew-formulae/akaikatana-repack",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }
 
@@ -1245,18 +1274,12 @@ Install-Binary "$Args"
 # CI that:
 #
 # * checks for a Git Tag that looks like a release
-# * creates a draft Github Release™ and fills in its text
 # * builds artifacts with cargo-dist (executable-zips, installers, hashes)
-# * uploads those artifacts to the Github Release™
-# * undrafts the Github Release™ on success
+# * uploads those artifacts to temporary workflow zip
+# * on success, uploads the artifacts to a Github Release™
 #
-# Note that the Github Release™ will be created before the artifacts,
-# so there will be a few minutes where the release has no artifacts
-# and then they will slowly trickle in, possibly failing. To make
-# this more pleasant we mark the release as a "draft" until all
-# artifacts have been successfully uploaded. This allows you to
-# choose what to do with partial successes and avoids spamming
-# anyone with notifications before the release is actually ready.
+# Note that the Github Release™ will be created with a generated
+# title/body based on your changelogs.
 name: Release
 
 permissions:
@@ -1287,15 +1310,17 @@ on:
   push:
     tags:
       - '**[0-9]+.[0-9]+.[0-9]+*'
+  pull_request:
 
 jobs:
   # Run 'cargo dist plan' to determine what tasks we need to do
-  # and create a draft github release with the computed title/body
   plan:
     runs-on: ubuntu-latest
     outputs:
-      has-releases: ${{ steps.plan.outputs.has-releases }}
-      releases: ${{ steps.plan.outputs.releases }}
+      val: ${{ steps.plan.outputs.manifest }}
+      tag: ${{ !github.event.pull_request && github.ref_name || '' }}
+      tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
+      publishing: ${{ !github.event.pull_request }}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
@@ -1308,49 +1333,35 @@ jobs:
         run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
       - id: plan
         run: |
-          cargo dist plan --tag=${{ github.ref_name }} --output-format=json > dist-manifest.json
-          echo "dist plan ran successfully"
+          cargo dist plan ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} --output-format=json > dist-manifest.json
+          echo "cargo dist plan ran successfully"
           cat dist-manifest.json
-
-          # Create the Github Release™ based on what cargo-dist thinks it should be
-          ANNOUNCEMENT_TITLE=$(jq --raw-output ".announcement_title" dist-manifest.json)
-          IS_PRERELEASE=$(jq --raw-output ".announcement_is_prerelease" dist-manifest.json)
-          jq --raw-output ".announcement_github_body" dist-manifest.json > new_dist_announcement.md
-          gh release create ${{ github.ref_name }} --draft --prerelease="$IS_PRERELEASE" --title="$ANNOUNCEMENT_TITLE" --notes-file=new_dist_announcement.md
-          echo "created announcement!"
-
-          # Upload the manifest to the Github Release™
-          gh release upload ${{ github.ref_name }} dist-manifest.json
-          echo "uploaded manifest!"
-
-          # Disable all the upload-artifacts tasks if we have no actual releases
-          HAS_RELEASES=$(jq --raw-output ".releases != null" dist-manifest.json)
-          echo "has-releases=$HAS_RELEASES" >> "$GITHUB_OUTPUT"
-          echo "releases=$(jq --compact-output ".releases" dist-manifest.json)" >> "$GITHUB_OUTPUT"
+          echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
+      - name: "Upload dist-manifest.json"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: dist-manifest.json
 
   # Build and packages all the platform-specific things
   upload-local-artifacts:
     # Let the initial task tell us to not run (currently very blunt)
     needs: plan
-    if: ${{ needs.plan.outputs.has-releases == 'true' }}
+    if: ${{ fromJson(needs.plan.outputs.val).releases != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
     strategy:
       fail-fast: false
-      matrix:
-        # For these target platforms
-        include:
-        - os: "macos-11"
-          dist-args: "--artifacts=local --target=aarch64-apple-darwin"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-        - os: "macos-11"
-          dist-args: "--artifacts=local --target=x86_64-apple-darwin"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-        - os: "windows-2019"
-          dist-args: "--artifacts=local --target=x86_64-pc-windows-msvc"
-          install-dist: "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex"
-        - os: "ubuntu-20.04"
-          dist-args: "--artifacts=local --target=x86_64-unknown-linux-gnu"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-    runs-on: ${{ matrix.os }}
+      # Target platforms/runners are computed by cargo-dist in create-release.
+      # Each member of the matrix has the following arguments:
+      #
+      # - runner: the github runner
+      # - dist-args: cli flags to pass to cargo dist
+      # - install-dist: expression to run to install cargo-dist on the runner
+      #
+      # Typically there will be:
+      # - 1 "global" task that builds universal installers
+      # - N "local" tasks that build each platform's binaries and platform-specific installers
+      matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
+    runs-on: ${{ matrix.runner }}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
@@ -1359,29 +1370,33 @@ jobs:
           submodules: recursive
       - name: Install Rust
         run: rustup update "1.67.1" --no-self-update && rustup default "1.67.1"
+      - uses: swatinem/rust-cache@v2
       - name: Install cargo-dist
-        run: ${{ matrix.install-dist }}
-      - name: Run cargo-dist
-        # This logic is a bit janky because it's trying to be a polyglot between
-        # powershell and bash since this will run on windows, macos, and linux!
-        # The two platforms don't agree on how to talk about env vars but they
-        # do agree on 'cat' and '$()' so we use that to marshal values between commands.
+        run: ${{ matrix.install_dist }}
+      - id: cargo-dist
+        # We force bash here just because github makes it really hard to get values up
+        # to "real" actions without writing to env-vars, and writing to env-vars has
+        # inconsistent syntax between shell and powershell. cargo-dist and jq work fine
+        # in powershell.
+        shell: bash
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build --tag=${{ github.ref_name }} --output-format=json ${{ matrix.dist-args }} > dist-manifest.json
-          echo "dist ran successfully"
-          cat dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          echo "cargo dist ran successfully"
 
           # Parse out what we just built and upload it to the Github Release™
-          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt
-          echo "uploading..."
-          cat uploads.txt
-          gh release upload ${{ github.ref_name }} $(cat uploads.txt)
-          echo "uploaded!"
+          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
+          echo "EOF" >> "$GITHUB_OUTPUT"
+      - name: "Upload artifacts"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: ${{ steps.cargo-dist.outputs.paths }}
 
   # Build and package all the platform-agnostic(ish) things
   upload-global-artifacts:
-    needs: upload-local-artifacts
+    needs: [plan, upload-local-artifacts]
     runs-on: "ubuntu-20.04"
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1395,27 +1410,43 @@ jobs:
         run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
       # Get all the local artifacts for the global tasks to use (for e.g. checksums)
       - name: Fetch local artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: target/distrib/
+      - id: cargo-dist
+        shell: bash
         run: |
-          gh release download ${{ github.ref_name }} --dir target/distrib/
-      - name: Run cargo-dist
-        run: |
-          cargo dist build --tag=${{ github.ref_name }} --output-format=json "--artifacts=global" > dist-manifest.json
-          echo "dist ran successfully"
-          cat dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
+          echo "cargo dist ran successfully"
 
           # Parse out what we just built and upload it to the Github Release™
-          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt
-          echo "uploading..."
-          cat uploads.txt
-          gh release upload ${{ github.ref_name }} $(cat uploads.txt)
-          echo "uploaded!"
-
-  upload-homebrew-formula:
-    needs: [plan, upload-global-artifacts]
+          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
+          echo "EOF" >> "$GITHUB_OUTPUT"
+      - name: "Upload artifacts"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: ${{ steps.cargo-dist.outputs.paths }}
+
+  should-publish:
+    needs:
+      - plan
+      - upload-local-artifacts
+      - upload-global-artifacts
+    if: ${{ needs.plan.outputs.publishing == 'true' }}
+    runs-on: ubuntu-latest
+    steps:
+      - name: print tag
+        run: echo "ok we're publishing!"
+
+  publish-homebrew-formula:
+    needs: [plan, should-publish]
     runs-on: "ubuntu-20.04"
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      RELEASES: ${{ needs.plan.outputs.releases }}
+      PLAN: ${{ needs.plan.outputs.val }}
       GITHUB_USER: "axo bot"
       GITHUB_EMAIL: "admin+bot@axo.dev"
     steps:
@@ -1425,14 +1456,16 @@ jobs:
           token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
       # So we have access to the formula
       - name: Fetch local artifacts
-        run: |
-          gh release download ${{ github.ref_name }} --dir Formula --repo ${GITHUB_REPOSITORY} --clobber
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: Formula/
       - name: Commit formula files
         run: |
           git config --global user.name "${GITHUB_USER}"
           git config --global user.email "${GITHUB_EMAIL}"
 
-          for release in $(echo "$RELEASES" | jq --compact-output '.[]'); do
+          for release in $(echo "$PLAN" | jq --compact-output '.releases[]'); do
             name=$(echo "$release" | jq .app_name --raw-output)
             version=$(echo "$release" | jq .app_version --raw-output)
 
@@ -1441,11 +1474,9 @@ jobs:
           done
           git push
 
-  # Mark the Github Release™ as a non-draft now that everything has succeeded!
+  # Create a Github Release with all the results once everything is done,
   publish-release:
-    # Only run after all the other tasks, but it's ok if upload-artifacts was skipped
-    needs: [plan, upload-local-artifacts, upload-global-artifacts]
-    if: ${{ always() && needs.plan.result == 'success' && (needs.upload-local-artifacts.result == 'skipped' || needs.upload-local-artifacts.result == 'success') && (needs.upload-global-artifacts.result == 'skipped' || needs.upload-global-artifacts.result == 'success') }}
+    needs: [plan, should-publish]
     runs-on: ubuntu-latest
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1453,8 +1484,18 @@ jobs:
       - uses: actions/checkout@v3
         with:
           submodules: recursive
-      - name: mark release as non-draft
-        run: |
-          gh release edit ${{ github.ref_name }} --draft=false
+      - name: "Download artifacts"
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: artifacts
+      - name: Create Release
+        uses: ncipollo/release-action@v1
+        with:
+          tag: ${{ needs.plan.outputs.tag }}
+          name: ${{ fromJson(needs.plan.outputs.val).announcement_title }}
+          body: ${{ fromJson(needs.plan.outputs.val).announcement_github_body }}
+          prerelease: ${{ fromJson(needs.plan.outputs.val).announcement_is_prerelease }}
+          artifacts: "artifacts/*"
 
 
diff --git a/cargo-dist/tests/snapshots/akaikatana_repo_with_dot_git.snap b/cargo-dist/tests/snapshots/akaikatana_repo_with_dot_git.snap
index 700dcc18..6d0cd702 100644
--- a/cargo-dist/tests/snapshots/akaikatana_repo_with_dot_git.snap
+++ b/cargo-dist/tests/snapshots/akaikatana_repo_with_dot_git.snap
@@ -1235,6 +1235,35 @@ Install-Binary "$Args"
       "install_hint": "brew install mistydemeo/homebrew-formulae/akaikatana-repack",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }
 
@@ -1245,18 +1274,12 @@ Install-Binary "$Args"
 # CI that:
 #
 # * checks for a Git Tag that looks like a release
-# * creates a draft Github Release™ and fills in its text
 # * builds artifacts with cargo-dist (executable-zips, installers, hashes)
-# * uploads those artifacts to the Github Release™
-# * undrafts the Github Release™ on success
+# * uploads those artifacts to temporary workflow zip
+# * on success, uploads the artifacts to a Github Release™
 #
-# Note that the Github Release™ will be created before the artifacts,
-# so there will be a few minutes where the release has no artifacts
-# and then they will slowly trickle in, possibly failing. To make
-# this more pleasant we mark the release as a "draft" until all
-# artifacts have been successfully uploaded. This allows you to
-# choose what to do with partial successes and avoids spamming
-# anyone with notifications before the release is actually ready.
+# Note that the Github Release™ will be created with a generated
+# title/body based on your changelogs.
 name: Release
 
 permissions:
@@ -1287,15 +1310,17 @@ on:
   push:
     tags:
       - '**[0-9]+.[0-9]+.[0-9]+*'
+  pull_request:
 
 jobs:
   # Run 'cargo dist plan' to determine what tasks we need to do
-  # and create a draft github release with the computed title/body
   plan:
     runs-on: ubuntu-latest
     outputs:
-      has-releases: ${{ steps.plan.outputs.has-releases }}
-      releases: ${{ steps.plan.outputs.releases }}
+      val: ${{ steps.plan.outputs.manifest }}
+      tag: ${{ !github.event.pull_request && github.ref_name || '' }}
+      tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
+      publishing: ${{ !github.event.pull_request }}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
@@ -1308,49 +1333,35 @@ jobs:
         run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
       - id: plan
         run: |
-          cargo dist plan --tag=${{ github.ref_name }} --output-format=json > dist-manifest.json
-          echo "dist plan ran successfully"
+          cargo dist plan ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} --output-format=json > dist-manifest.json
+          echo "cargo dist plan ran successfully"
           cat dist-manifest.json
-
-          # Create the Github Release™ based on what cargo-dist thinks it should be
-          ANNOUNCEMENT_TITLE=$(jq --raw-output ".announcement_title" dist-manifest.json)
-          IS_PRERELEASE=$(jq --raw-output ".announcement_is_prerelease" dist-manifest.json)
-          jq --raw-output ".announcement_github_body" dist-manifest.json > new_dist_announcement.md
-          gh release create ${{ github.ref_name }} --draft --prerelease="$IS_PRERELEASE" --title="$ANNOUNCEMENT_TITLE" --notes-file=new_dist_announcement.md
-          echo "created announcement!"
-
-          # Upload the manifest to the Github Release™
-          gh release upload ${{ github.ref_name }} dist-manifest.json
-          echo "uploaded manifest!"
-
-          # Disable all the upload-artifacts tasks if we have no actual releases
-          HAS_RELEASES=$(jq --raw-output ".releases != null" dist-manifest.json)
-          echo "has-releases=$HAS_RELEASES" >> "$GITHUB_OUTPUT"
-          echo "releases=$(jq --compact-output ".releases" dist-manifest.json)" >> "$GITHUB_OUTPUT"
+          echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
+      - name: "Upload dist-manifest.json"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: dist-manifest.json
 
   # Build and packages all the platform-specific things
   upload-local-artifacts:
     # Let the initial task tell us to not run (currently very blunt)
     needs: plan
-    if: ${{ needs.plan.outputs.has-releases == 'true' }}
+    if: ${{ fromJson(needs.plan.outputs.val).releases != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
     strategy:
       fail-fast: false
-      matrix:
-        # For these target platforms
-        include:
-        - os: "macos-11"
-          dist-args: "--artifacts=local --target=aarch64-apple-darwin"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-        - os: "macos-11"
-          dist-args: "--artifacts=local --target=x86_64-apple-darwin"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-        - os: "windows-2019"
-          dist-args: "--artifacts=local --target=x86_64-pc-windows-msvc"
-          install-dist: "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex"
-        - os: "ubuntu-20.04"
-          dist-args: "--artifacts=local --target=x86_64-unknown-linux-gnu"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-    runs-on: ${{ matrix.os }}
+      # Target platforms/runners are computed by cargo-dist in create-release.
+      # Each member of the matrix has the following arguments:
+      #
+      # - runner: the github runner
+      # - dist-args: cli flags to pass to cargo dist
+      # - install-dist: expression to run to install cargo-dist on the runner
+      #
+      # Typically there will be:
+      # - 1 "global" task that builds universal installers
+      # - N "local" tasks that build each platform's binaries and platform-specific installers
+      matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
+    runs-on: ${{ matrix.runner }}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
@@ -1359,29 +1370,33 @@ jobs:
           submodules: recursive
       - name: Install Rust
         run: rustup update "1.67.1" --no-self-update && rustup default "1.67.1"
+      - uses: swatinem/rust-cache@v2
       - name: Install cargo-dist
-        run: ${{ matrix.install-dist }}
-      - name: Run cargo-dist
-        # This logic is a bit janky because it's trying to be a polyglot between
-        # powershell and bash since this will run on windows, macos, and linux!
-        # The two platforms don't agree on how to talk about env vars but they
-        # do agree on 'cat' and '$()' so we use that to marshal values between commands.
+        run: ${{ matrix.install_dist }}
+      - id: cargo-dist
+        # We force bash here just because github makes it really hard to get values up
+        # to "real" actions without writing to env-vars, and writing to env-vars has
+        # inconsistent syntax between shell and powershell. cargo-dist and jq work fine
+        # in powershell.
+        shell: bash
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build --tag=${{ github.ref_name }} --output-format=json ${{ matrix.dist-args }} > dist-manifest.json
-          echo "dist ran successfully"
-          cat dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          echo "cargo dist ran successfully"
 
           # Parse out what we just built and upload it to the Github Release™
-          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt
-          echo "uploading..."
-          cat uploads.txt
-          gh release upload ${{ github.ref_name }} $(cat uploads.txt)
-          echo "uploaded!"
+          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
+          echo "EOF" >> "$GITHUB_OUTPUT"
+      - name: "Upload artifacts"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: ${{ steps.cargo-dist.outputs.paths }}
 
   # Build and package all the platform-agnostic(ish) things
   upload-global-artifacts:
-    needs: upload-local-artifacts
+    needs: [plan, upload-local-artifacts]
     runs-on: "ubuntu-20.04"
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1395,27 +1410,43 @@ jobs:
         run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
       # Get all the local artifacts for the global tasks to use (for e.g. checksums)
       - name: Fetch local artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: target/distrib/
+      - id: cargo-dist
+        shell: bash
         run: |
-          gh release download ${{ github.ref_name }} --dir target/distrib/
-      - name: Run cargo-dist
-        run: |
-          cargo dist build --tag=${{ github.ref_name }} --output-format=json "--artifacts=global" > dist-manifest.json
-          echo "dist ran successfully"
-          cat dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
+          echo "cargo dist ran successfully"
 
           # Parse out what we just built and upload it to the Github Release™
-          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt
-          echo "uploading..."
-          cat uploads.txt
-          gh release upload ${{ github.ref_name }} $(cat uploads.txt)
-          echo "uploaded!"
-
-  upload-homebrew-formula:
-    needs: [plan, upload-global-artifacts]
+          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
+          echo "EOF" >> "$GITHUB_OUTPUT"
+      - name: "Upload artifacts"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: ${{ steps.cargo-dist.outputs.paths }}
+
+  should-publish:
+    needs:
+      - plan
+      - upload-local-artifacts
+      - upload-global-artifacts
+    if: ${{ needs.plan.outputs.publishing == 'true' }}
+    runs-on: ubuntu-latest
+    steps:
+      - name: print tag
+        run: echo "ok we're publishing!"
+
+  publish-homebrew-formula:
+    needs: [plan, should-publish]
     runs-on: "ubuntu-20.04"
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      RELEASES: ${{ needs.plan.outputs.releases }}
+      PLAN: ${{ needs.plan.outputs.val }}
       GITHUB_USER: "axo bot"
       GITHUB_EMAIL: "admin+bot@axo.dev"
     steps:
@@ -1425,14 +1456,16 @@ jobs:
           token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
       # So we have access to the formula
       - name: Fetch local artifacts
-        run: |
-          gh release download ${{ github.ref_name }} --dir Formula --repo ${GITHUB_REPOSITORY} --clobber
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: Formula/
       - name: Commit formula files
         run: |
           git config --global user.name "${GITHUB_USER}"
           git config --global user.email "${GITHUB_EMAIL}"
 
-          for release in $(echo "$RELEASES" | jq --compact-output '.[]'); do
+          for release in $(echo "$PLAN" | jq --compact-output '.releases[]'); do
             name=$(echo "$release" | jq .app_name --raw-output)
             version=$(echo "$release" | jq .app_version --raw-output)
 
@@ -1441,11 +1474,9 @@ jobs:
           done
           git push
 
-  # Mark the Github Release™ as a non-draft now that everything has succeeded!
+  # Create a Github Release with all the results once everything is done,
   publish-release:
-    # Only run after all the other tasks, but it's ok if upload-artifacts was skipped
-    needs: [plan, upload-local-artifacts, upload-global-artifacts]
-    if: ${{ always() && needs.plan.result == 'success' && (needs.upload-local-artifacts.result == 'skipped' || needs.upload-local-artifacts.result == 'success') && (needs.upload-global-artifacts.result == 'skipped' || needs.upload-global-artifacts.result == 'success') }}
+    needs: [plan, should-publish]
     runs-on: ubuntu-latest
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1453,8 +1484,18 @@ jobs:
       - uses: actions/checkout@v3
         with:
           submodules: recursive
-      - name: mark release as non-draft
-        run: |
-          gh release edit ${{ github.ref_name }} --draft=false
+      - name: "Download artifacts"
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: artifacts
+      - name: Create Release
+        uses: ncipollo/release-action@v1
+        with:
+          tag: ${{ needs.plan.outputs.tag }}
+          name: ${{ fromJson(needs.plan.outputs.val).announcement_title }}
+          body: ${{ fromJson(needs.plan.outputs.val).announcement_github_body }}
+          prerelease: ${{ fromJson(needs.plan.outputs.val).announcement_is_prerelease }}
+          artifacts: "artifacts/*"
 
 
diff --git a/cargo-dist/tests/snapshots/axolotlsay_basic.snap b/cargo-dist/tests/snapshots/axolotlsay_basic.snap
index 7f306115..779c1b15 100644
--- a/cargo-dist/tests/snapshots/axolotlsay_basic.snap
+++ b/cargo-dist/tests/snapshots/axolotlsay_basic.snap
@@ -2108,6 +2108,35 @@ maybeInstall(true).then(run);
       "install_hint": "brew install axodotdev/homebrew-packages/axolotlsay",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }
 
@@ -2118,18 +2147,12 @@ maybeInstall(true).then(run);
 # CI that:
 #
 # * checks for a Git Tag that looks like a release
-# * creates a draft Github Release™ and fills in its text
 # * builds artifacts with cargo-dist (executable-zips, installers, hashes)
-# * uploads those artifacts to the Github Release™
-# * undrafts the Github Release™ on success
+# * uploads those artifacts to temporary workflow zip
+# * on success, uploads the artifacts to a Github Release™
 #
-# Note that the Github Release™ will be created before the artifacts,
-# so there will be a few minutes where the release has no artifacts
-# and then they will slowly trickle in, possibly failing. To make
-# this more pleasant we mark the release as a "draft" until all
-# artifacts have been successfully uploaded. This allows you to
-# choose what to do with partial successes and avoids spamming
-# anyone with notifications before the release is actually ready.
+# Note that the Github Release™ will be created with a generated
+# title/body based on your changelogs.
 name: Release
 
 permissions:
@@ -2160,15 +2183,17 @@ on:
   push:
     tags:
       - '**[0-9]+.[0-9]+.[0-9]+*'
+  pull_request:
 
 jobs:
   # Run 'cargo dist plan' to determine what tasks we need to do
-  # and create a draft github release with the computed title/body
   plan:
     runs-on: ubuntu-latest
     outputs:
-      has-releases: ${{ steps.plan.outputs.has-releases }}
-      releases: ${{ steps.plan.outputs.releases }}
+      val: ${{ steps.plan.outputs.manifest }}
+      tag: ${{ !github.event.pull_request && github.ref_name || '' }}
+      tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
+      publishing: ${{ !github.event.pull_request }}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
@@ -2179,78 +2204,68 @@ jobs:
         run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
       - id: plan
         run: |
-          cargo dist plan --tag=${{ github.ref_name }} --output-format=json > dist-manifest.json
-          echo "dist plan ran successfully"
+          cargo dist plan ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} --output-format=json > dist-manifest.json
+          echo "cargo dist plan ran successfully"
           cat dist-manifest.json
-
-          # Create the Github Release™ based on what cargo-dist thinks it should be
-          ANNOUNCEMENT_TITLE=$(jq --raw-output ".announcement_title" dist-manifest.json)
-          IS_PRERELEASE=$(jq --raw-output ".announcement_is_prerelease" dist-manifest.json)
-          jq --raw-output ".announcement_github_body" dist-manifest.json > new_dist_announcement.md
-          gh release create ${{ github.ref_name }} --draft --prerelease="$IS_PRERELEASE" --title="$ANNOUNCEMENT_TITLE" --notes-file=new_dist_announcement.md
-          echo "created announcement!"
-
-          # Upload the manifest to the Github Release™
-          gh release upload ${{ github.ref_name }} dist-manifest.json
-          echo "uploaded manifest!"
-
-          # Disable all the upload-artifacts tasks if we have no actual releases
-          HAS_RELEASES=$(jq --raw-output ".releases != null" dist-manifest.json)
-          echo "has-releases=$HAS_RELEASES" >> "$GITHUB_OUTPUT"
-          echo "releases=$(jq --compact-output ".releases" dist-manifest.json)" >> "$GITHUB_OUTPUT"
+          echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
+      - name: "Upload dist-manifest.json"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: dist-manifest.json
 
   # Build and packages all the platform-specific things
   upload-local-artifacts:
     # Let the initial task tell us to not run (currently very blunt)
     needs: plan
-    if: ${{ needs.plan.outputs.has-releases == 'true' }}
+    if: ${{ fromJson(needs.plan.outputs.val).releases != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
     strategy:
       fail-fast: false
-      matrix:
-        # For these target platforms
-        include:
-        - os: "macos-11"
-          dist-args: "--artifacts=local --target=aarch64-apple-darwin"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-        - os: "macos-11"
-          dist-args: "--artifacts=local --target=x86_64-apple-darwin"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-        - os: "windows-2019"
-          dist-args: "--artifacts=local --target=x86_64-pc-windows-msvc"
-          install-dist: "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex"
-        - os: "ubuntu-20.04"
-          dist-args: "--artifacts=local --target=x86_64-unknown-linux-gnu"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-    runs-on: ${{ matrix.os }}
+      # Target platforms/runners are computed by cargo-dist in create-release.
+      # Each member of the matrix has the following arguments:
+      #
+      # - runner: the github runner
+      # - dist-args: cli flags to pass to cargo dist
+      # - install-dist: expression to run to install cargo-dist on the runner
+      #
+      # Typically there will be:
+      # - 1 "global" task that builds universal installers
+      # - N "local" tasks that build each platform's binaries and platform-specific installers
+      matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
+    runs-on: ${{ matrix.runner }}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
       - uses: actions/checkout@v3
         with:
           submodules: recursive
+      - uses: swatinem/rust-cache@v2
       - name: Install cargo-dist
-        run: ${{ matrix.install-dist }}
-      - name: Run cargo-dist
-        # This logic is a bit janky because it's trying to be a polyglot between
-        # powershell and bash since this will run on windows, macos, and linux!
-        # The two platforms don't agree on how to talk about env vars but they
-        # do agree on 'cat' and '$()' so we use that to marshal values between commands.
+        run: ${{ matrix.install_dist }}
+      - id: cargo-dist
+        # We force bash here just because github makes it really hard to get values up
+        # to "real" actions without writing to env-vars, and writing to env-vars has
+        # inconsistent syntax between shell and powershell. cargo-dist and jq work fine
+        # in powershell.
+        shell: bash
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build --tag=${{ github.ref_name }} --output-format=json ${{ matrix.dist-args }} > dist-manifest.json
-          echo "dist ran successfully"
-          cat dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          echo "cargo dist ran successfully"
 
           # Parse out what we just built and upload it to the Github Release™
-          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt
-          echo "uploading..."
-          cat uploads.txt
-          gh release upload ${{ github.ref_name }} $(cat uploads.txt)
-          echo "uploaded!"
+          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
+          echo "EOF" >> "$GITHUB_OUTPUT"
+      - name: "Upload artifacts"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: ${{ steps.cargo-dist.outputs.paths }}
 
   # Build and package all the platform-agnostic(ish) things
   upload-global-artifacts:
-    needs: upload-local-artifacts
+    needs: [plan, upload-local-artifacts]
     runs-on: "ubuntu-20.04"
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -2262,27 +2277,43 @@ jobs:
         run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
       # Get all the local artifacts for the global tasks to use (for e.g. checksums)
       - name: Fetch local artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: target/distrib/
+      - id: cargo-dist
+        shell: bash
         run: |
-          gh release download ${{ github.ref_name }} --dir target/distrib/
-      - name: Run cargo-dist
-        run: |
-          cargo dist build --tag=${{ github.ref_name }} --output-format=json "--artifacts=global" > dist-manifest.json
-          echo "dist ran successfully"
-          cat dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
+          echo "cargo dist ran successfully"
 
           # Parse out what we just built and upload it to the Github Release™
-          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt
-          echo "uploading..."
-          cat uploads.txt
-          gh release upload ${{ github.ref_name }} $(cat uploads.txt)
-          echo "uploaded!"
-
-  upload-homebrew-formula:
-    needs: [plan, upload-global-artifacts]
+          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
+          echo "EOF" >> "$GITHUB_OUTPUT"
+      - name: "Upload artifacts"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: ${{ steps.cargo-dist.outputs.paths }}
+
+  should-publish:
+    needs:
+      - plan
+      - upload-local-artifacts
+      - upload-global-artifacts
+    if: ${{ needs.plan.outputs.publishing == 'true' }}
+    runs-on: ubuntu-latest
+    steps:
+      - name: print tag
+        run: echo "ok we're publishing!"
+
+  publish-homebrew-formula:
+    needs: [plan, should-publish]
     runs-on: "ubuntu-20.04"
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      RELEASES: ${{ needs.plan.outputs.releases }}
+      PLAN: ${{ needs.plan.outputs.val }}
       GITHUB_USER: "axo bot"
       GITHUB_EMAIL: "admin+bot@axo.dev"
     steps:
@@ -2292,14 +2323,16 @@ jobs:
           token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
       # So we have access to the formula
       - name: Fetch local artifacts
-        run: |
-          gh release download ${{ github.ref_name }} --dir Formula --repo ${GITHUB_REPOSITORY} --clobber
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: Formula/
       - name: Commit formula files
         run: |
           git config --global user.name "${GITHUB_USER}"
           git config --global user.email "${GITHUB_EMAIL}"
 
-          for release in $(echo "$RELEASES" | jq --compact-output '.[]'); do
+          for release in $(echo "$PLAN" | jq --compact-output '.releases[]'); do
             name=$(echo "$release" | jq .app_name --raw-output)
             version=$(echo "$release" | jq .app_version --raw-output)
 
@@ -2308,11 +2341,9 @@ jobs:
           done
           git push
 
-  # Mark the Github Release™ as a non-draft now that everything has succeeded!
+  # Create a Github Release with all the results once everything is done,
   publish-release:
-    # Only run after all the other tasks, but it's ok if upload-artifacts was skipped
-    needs: [plan, upload-local-artifacts, upload-global-artifacts]
-    if: ${{ always() && needs.plan.result == 'success' && (needs.upload-local-artifacts.result == 'skipped' || needs.upload-local-artifacts.result == 'success') && (needs.upload-global-artifacts.result == 'skipped' || needs.upload-global-artifacts.result == 'success') }}
+    needs: [plan, should-publish]
     runs-on: ubuntu-latest
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -2320,8 +2351,18 @@ jobs:
       - uses: actions/checkout@v3
         with:
           submodules: recursive
-      - name: mark release as non-draft
-        run: |
-          gh release edit ${{ github.ref_name }} --draft=false
+      - name: "Download artifacts"
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: artifacts
+      - name: Create Release
+        uses: ncipollo/release-action@v1
+        with:
+          tag: ${{ needs.plan.outputs.tag }}
+          name: ${{ fromJson(needs.plan.outputs.val).announcement_title }}
+          body: ${{ fromJson(needs.plan.outputs.val).announcement_github_body }}
+          prerelease: ${{ fromJson(needs.plan.outputs.val).announcement_is_prerelease }}
+          artifacts: "artifacts/*"
 
 
diff --git a/cargo-dist/tests/snapshots/axolotlsay_edit_existing.snap b/cargo-dist/tests/snapshots/axolotlsay_edit_existing.snap
index 4419773c..b2cf341c 100644
--- a/cargo-dist/tests/snapshots/axolotlsay_edit_existing.snap
+++ b/cargo-dist/tests/snapshots/axolotlsay_edit_existing.snap
@@ -2108,6 +2108,35 @@ maybeInstall(true).then(run);
       "install_hint": "brew install axodotdev/homebrew-packages/axolotlsay",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }
 
@@ -2119,8 +2148,8 @@ maybeInstall(true).then(run);
 #
 # * checks for a Git Tag that looks like a release
 # * builds artifacts with cargo-dist (executable-zips, installers, hashes)
-# * uploads those artifacts to the Github Release™
-# * undrafts the Github Release™ on success
+# * uploads those artifacts to temporary workflow zip
+# * on success, uploads the artifacts to a Github Release™
 #
 # Note that a Github Release™ with this tag is assumed to exist as a draft
 # with the appropriate title/body, and will be undrafted for you.
@@ -2154,14 +2183,17 @@ on:
   push:
     tags:
       - '**[0-9]+.[0-9]+.[0-9]+*'
+  pull_request:
 
 jobs:
   # Run 'cargo dist plan' to determine what tasks we need to do
   plan:
     runs-on: ubuntu-latest
     outputs:
-      has-releases: ${{ steps.plan.outputs.has-releases }}
-      releases: ${{ steps.plan.outputs.releases }}
+      val: ${{ steps.plan.outputs.manifest }}
+      tag: ${{ !github.event.pull_request && github.ref_name || '' }}
+      tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
+      publishing: ${{ !github.event.pull_request }}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
@@ -2172,73 +2204,68 @@ jobs:
         run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
       - id: plan
         run: |
-          cargo dist plan --tag=${{ github.ref_name }} --output-format=json > dist-manifest.json
-          echo "dist plan ran successfully"
+          cargo dist plan ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} --output-format=json > dist-manifest.json
+          echo "cargo dist plan ran successfully"
           cat dist-manifest.json
-
-          # We're assuming a draft Github Release™ with the desired title/tag/body already exists
-
-          # Upload the manifest to the Github Release™
-          gh release upload ${{ github.ref_name }} dist-manifest.json
-          echo "uploaded manifest!"
-
-          # Disable all the upload-artifacts tasks if we have no actual releases
-          HAS_RELEASES=$(jq --raw-output ".releases != null" dist-manifest.json)
-          echo "has-releases=$HAS_RELEASES" >> "$GITHUB_OUTPUT"
-          echo "releases=$(jq --compact-output ".releases" dist-manifest.json)" >> "$GITHUB_OUTPUT"
+          echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
+      - name: "Upload dist-manifest.json"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: dist-manifest.json
 
   # Build and packages all the platform-specific things
   upload-local-artifacts:
     # Let the initial task tell us to not run (currently very blunt)
     needs: plan
-    if: ${{ needs.plan.outputs.has-releases == 'true' }}
+    if: ${{ fromJson(needs.plan.outputs.val).releases != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
     strategy:
       fail-fast: false
-      matrix:
-        # For these target platforms
-        include:
-        - os: "macos-11"
-          dist-args: "--artifacts=local --target=aarch64-apple-darwin"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-        - os: "macos-11"
-          dist-args: "--artifacts=local --target=x86_64-apple-darwin"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-        - os: "windows-2019"
-          dist-args: "--artifacts=local --target=x86_64-pc-windows-msvc"
-          install-dist: "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex"
-        - os: "ubuntu-20.04"
-          dist-args: "--artifacts=local --target=x86_64-unknown-linux-gnu"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-    runs-on: ${{ matrix.os }}
+      # Target platforms/runners are computed by cargo-dist in create-release.
+      # Each member of the matrix has the following arguments:
+      #
+      # - runner: the github runner
+      # - dist-args: cli flags to pass to cargo dist
+      # - install-dist: expression to run to install cargo-dist on the runner
+      #
+      # Typically there will be:
+      # - 1 "global" task that builds universal installers
+      # - N "local" tasks that build each platform's binaries and platform-specific installers
+      matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
+    runs-on: ${{ matrix.runner }}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
       - uses: actions/checkout@v3
         with:
           submodules: recursive
+      - uses: swatinem/rust-cache@v2
       - name: Install cargo-dist
-        run: ${{ matrix.install-dist }}
-      - name: Run cargo-dist
-        # This logic is a bit janky because it's trying to be a polyglot between
-        # powershell and bash since this will run on windows, macos, and linux!
-        # The two platforms don't agree on how to talk about env vars but they
-        # do agree on 'cat' and '$()' so we use that to marshal values between commands.
+        run: ${{ matrix.install_dist }}
+      - id: cargo-dist
+        # We force bash here just because github makes it really hard to get values up
+        # to "real" actions without writing to env-vars, and writing to env-vars has
+        # inconsistent syntax between shell and powershell. cargo-dist and jq work fine
+        # in powershell.
+        shell: bash
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build --tag=${{ github.ref_name }} --output-format=json ${{ matrix.dist-args }} > dist-manifest.json
-          echo "dist ran successfully"
-          cat dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          echo "cargo dist ran successfully"
 
           # Parse out what we just built and upload it to the Github Release™
-          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt
-          echo "uploading..."
-          cat uploads.txt
-          gh release upload ${{ github.ref_name }} $(cat uploads.txt)
-          echo "uploaded!"
+          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
+          echo "EOF" >> "$GITHUB_OUTPUT"
+      - name: "Upload artifacts"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: ${{ steps.cargo-dist.outputs.paths }}
 
   # Build and package all the platform-agnostic(ish) things
   upload-global-artifacts:
-    needs: upload-local-artifacts
+    needs: [plan, upload-local-artifacts]
     runs-on: "ubuntu-20.04"
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -2250,27 +2277,43 @@ jobs:
         run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
       # Get all the local artifacts for the global tasks to use (for e.g. checksums)
       - name: Fetch local artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: target/distrib/
+      - id: cargo-dist
+        shell: bash
         run: |
-          gh release download ${{ github.ref_name }} --dir target/distrib/
-      - name: Run cargo-dist
-        run: |
-          cargo dist build --tag=${{ github.ref_name }} --output-format=json "--artifacts=global" > dist-manifest.json
-          echo "dist ran successfully"
-          cat dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
+          echo "cargo dist ran successfully"
 
           # Parse out what we just built and upload it to the Github Release™
-          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt
-          echo "uploading..."
-          cat uploads.txt
-          gh release upload ${{ github.ref_name }} $(cat uploads.txt)
-          echo "uploaded!"
-
-  upload-homebrew-formula:
-    needs: [plan, upload-global-artifacts]
+          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
+          echo "EOF" >> "$GITHUB_OUTPUT"
+      - name: "Upload artifacts"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: ${{ steps.cargo-dist.outputs.paths }}
+
+  should-publish:
+    needs:
+      - plan
+      - upload-local-artifacts
+      - upload-global-artifacts
+    if: ${{ needs.plan.outputs.publishing == 'true' }}
+    runs-on: ubuntu-latest
+    steps:
+      - name: print tag
+        run: echo "ok we're publishing!"
+
+  publish-homebrew-formula:
+    needs: [plan, should-publish]
     runs-on: "ubuntu-20.04"
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      RELEASES: ${{ needs.plan.outputs.releases }}
+      PLAN: ${{ needs.plan.outputs.val }}
       GITHUB_USER: "axo bot"
       GITHUB_EMAIL: "admin+bot@axo.dev"
     steps:
@@ -2280,14 +2323,16 @@ jobs:
           token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
       # So we have access to the formula
       - name: Fetch local artifacts
-        run: |
-          gh release download ${{ github.ref_name }} --dir Formula --repo ${GITHUB_REPOSITORY} --clobber
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: Formula/
       - name: Commit formula files
         run: |
           git config --global user.name "${GITHUB_USER}"
           git config --global user.email "${GITHUB_EMAIL}"
 
-          for release in $(echo "$RELEASES" | jq --compact-output '.[]'); do
+          for release in $(echo "$PLAN" | jq --compact-output '.releases[]'); do
             name=$(echo "$release" | jq .app_name --raw-output)
             version=$(echo "$release" | jq .app_version --raw-output)
 
@@ -2296,11 +2341,9 @@ jobs:
           done
           git push
 
-  # Mark the Github Release™ as a non-draft now that everything has succeeded!
+  # Create a Github Release with all the results once everything is done,
   publish-release:
-    # Only run after all the other tasks, but it's ok if upload-artifacts was skipped
-    needs: [plan, upload-local-artifacts, upload-global-artifacts]
-    if: ${{ always() && needs.plan.result == 'success' && (needs.upload-local-artifacts.result == 'skipped' || needs.upload-local-artifacts.result == 'success') && (needs.upload-global-artifacts.result == 'skipped' || needs.upload-global-artifacts.result == 'success') }}
+    needs: [plan, should-publish]
     runs-on: ubuntu-latest
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -2308,8 +2351,20 @@ jobs:
       - uses: actions/checkout@v3
         with:
           submodules: recursive
-      - name: mark release as non-draft
-        run: |
-          gh release edit ${{ github.ref_name }} --draft=false
+      - name: "Download artifacts"
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: artifacts
+      - name: Create Release
+        uses: ncipollo/release-action@v1
+        with:
+          tag: ${{ needs.plan.outputs.tag }}
+          allowUpdates: true
+          updateOnlyUnreleased: true
+          omitBodyDuringUpdate: true
+          omitNameDuringUpdate: true
+          prerelease: ${{ fromJson(needs.plan.outputs.val).announcement_is_prerelease }}
+          artifacts: "artifacts/*"
 
 
diff --git a/cargo-dist/tests/snapshots/axolotlsay_no_homebrew_publish.snap b/cargo-dist/tests/snapshots/axolotlsay_no_homebrew_publish.snap
index 1a88e6e4..0376305e 100644
--- a/cargo-dist/tests/snapshots/axolotlsay_no_homebrew_publish.snap
+++ b/cargo-dist/tests/snapshots/axolotlsay_no_homebrew_publish.snap
@@ -2108,6 +2108,35 @@ maybeInstall(true).then(run);
       "install_hint": "brew install axodotdev/homebrew-packages/axolotlsay",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }
 
@@ -2118,18 +2147,12 @@ maybeInstall(true).then(run);
 # CI that:
 #
 # * checks for a Git Tag that looks like a release
-# * creates a draft Github Release™ and fills in its text
 # * builds artifacts with cargo-dist (executable-zips, installers, hashes)
-# * uploads those artifacts to the Github Release™
-# * undrafts the Github Release™ on success
+# * uploads those artifacts to temporary workflow zip
+# * on success, uploads the artifacts to a Github Release™
 #
-# Note that the Github Release™ will be created before the artifacts,
-# so there will be a few minutes where the release has no artifacts
-# and then they will slowly trickle in, possibly failing. To make
-# this more pleasant we mark the release as a "draft" until all
-# artifacts have been successfully uploaded. This allows you to
-# choose what to do with partial successes and avoids spamming
-# anyone with notifications before the release is actually ready.
+# Note that the Github Release™ will be created with a generated
+# title/body based on your changelogs.
 name: Release
 
 permissions:
@@ -2160,15 +2183,17 @@ on:
   push:
     tags:
       - '**[0-9]+.[0-9]+.[0-9]+*'
+  pull_request:
 
 jobs:
   # Run 'cargo dist plan' to determine what tasks we need to do
-  # and create a draft github release with the computed title/body
   plan:
     runs-on: ubuntu-latest
     outputs:
-      has-releases: ${{ steps.plan.outputs.has-releases }}
-      releases: ${{ steps.plan.outputs.releases }}
+      val: ${{ steps.plan.outputs.manifest }}
+      tag: ${{ !github.event.pull_request && github.ref_name || '' }}
+      tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
+      publishing: ${{ !github.event.pull_request }}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
@@ -2179,78 +2204,68 @@ jobs:
         run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
       - id: plan
         run: |
-          cargo dist plan --tag=${{ github.ref_name }} --output-format=json > dist-manifest.json
-          echo "dist plan ran successfully"
+          cargo dist plan ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} --output-format=json > dist-manifest.json
+          echo "cargo dist plan ran successfully"
           cat dist-manifest.json
-
-          # Create the Github Release™ based on what cargo-dist thinks it should be
-          ANNOUNCEMENT_TITLE=$(jq --raw-output ".announcement_title" dist-manifest.json)
-          IS_PRERELEASE=$(jq --raw-output ".announcement_is_prerelease" dist-manifest.json)
-          jq --raw-output ".announcement_github_body" dist-manifest.json > new_dist_announcement.md
-          gh release create ${{ github.ref_name }} --draft --prerelease="$IS_PRERELEASE" --title="$ANNOUNCEMENT_TITLE" --notes-file=new_dist_announcement.md
-          echo "created announcement!"
-
-          # Upload the manifest to the Github Release™
-          gh release upload ${{ github.ref_name }} dist-manifest.json
-          echo "uploaded manifest!"
-
-          # Disable all the upload-artifacts tasks if we have no actual releases
-          HAS_RELEASES=$(jq --raw-output ".releases != null" dist-manifest.json)
-          echo "has-releases=$HAS_RELEASES" >> "$GITHUB_OUTPUT"
-          echo "releases=$(jq --compact-output ".releases" dist-manifest.json)" >> "$GITHUB_OUTPUT"
+          echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
+      - name: "Upload dist-manifest.json"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: dist-manifest.json
 
   # Build and packages all the platform-specific things
   upload-local-artifacts:
     # Let the initial task tell us to not run (currently very blunt)
     needs: plan
-    if: ${{ needs.plan.outputs.has-releases == 'true' }}
+    if: ${{ fromJson(needs.plan.outputs.val).releases != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
     strategy:
       fail-fast: false
-      matrix:
-        # For these target platforms
-        include:
-        - os: "macos-11"
-          dist-args: "--artifacts=local --target=aarch64-apple-darwin"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-        - os: "macos-11"
-          dist-args: "--artifacts=local --target=x86_64-apple-darwin"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-        - os: "windows-2019"
-          dist-args: "--artifacts=local --target=x86_64-pc-windows-msvc"
-          install-dist: "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex"
-        - os: "ubuntu-20.04"
-          dist-args: "--artifacts=local --target=x86_64-unknown-linux-gnu"
-          install-dist: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
-    runs-on: ${{ matrix.os }}
+      # Target platforms/runners are computed by cargo-dist in create-release.
+      # Each member of the matrix has the following arguments:
+      #
+      # - runner: the github runner
+      # - dist-args: cli flags to pass to cargo dist
+      # - install-dist: expression to run to install cargo-dist on the runner
+      #
+      # Typically there will be:
+      # - 1 "global" task that builds universal installers
+      # - N "local" tasks that build each platform's binaries and platform-specific installers
+      matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
+    runs-on: ${{ matrix.runner }}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
       - uses: actions/checkout@v3
         with:
           submodules: recursive
+      - uses: swatinem/rust-cache@v2
       - name: Install cargo-dist
-        run: ${{ matrix.install-dist }}
-      - name: Run cargo-dist
-        # This logic is a bit janky because it's trying to be a polyglot between
-        # powershell and bash since this will run on windows, macos, and linux!
-        # The two platforms don't agree on how to talk about env vars but they
-        # do agree on 'cat' and '$()' so we use that to marshal values between commands.
+        run: ${{ matrix.install_dist }}
+      - id: cargo-dist
+        # We force bash here just because github makes it really hard to get values up
+        # to "real" actions without writing to env-vars, and writing to env-vars has
+        # inconsistent syntax between shell and powershell. cargo-dist and jq work fine
+        # in powershell.
+        shell: bash
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build --tag=${{ github.ref_name }} --output-format=json ${{ matrix.dist-args }} > dist-manifest.json
-          echo "dist ran successfully"
-          cat dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          echo "cargo dist ran successfully"
 
           # Parse out what we just built and upload it to the Github Release™
-          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt
-          echo "uploading..."
-          cat uploads.txt
-          gh release upload ${{ github.ref_name }} $(cat uploads.txt)
-          echo "uploaded!"
+          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
+          echo "EOF" >> "$GITHUB_OUTPUT"
+      - name: "Upload artifacts"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: ${{ steps.cargo-dist.outputs.paths }}
 
   # Build and package all the platform-agnostic(ish) things
   upload-global-artifacts:
-    needs: upload-local-artifacts
+    needs: [plan, upload-local-artifacts]
     runs-on: "ubuntu-20.04"
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -2262,26 +2277,40 @@ jobs:
         run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
       # Get all the local artifacts for the global tasks to use (for e.g. checksums)
       - name: Fetch local artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: target/distrib/
+      - id: cargo-dist
+        shell: bash
         run: |
-          gh release download ${{ github.ref_name }} --dir target/distrib/
-      - name: Run cargo-dist
-        run: |
-          cargo dist build --tag=${{ github.ref_name }} --output-format=json "--artifacts=global" > dist-manifest.json
-          echo "dist ran successfully"
-          cat dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
+          echo "cargo dist ran successfully"
 
           # Parse out what we just built and upload it to the Github Release™
-          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt
-          echo "uploading..."
-          cat uploads.txt
-          gh release upload ${{ github.ref_name }} $(cat uploads.txt)
-          echo "uploaded!"
+          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
+          echo "EOF" >> "$GITHUB_OUTPUT"
+      - name: "Upload artifacts"
+        uses: actions/upload-artifact@v3
+        with:
+          name: artifacts
+          path: ${{ steps.cargo-dist.outputs.paths }}
+
+  should-publish:
+    needs:
+      - plan
+      - upload-local-artifacts
+      - upload-global-artifacts
+    if: ${{ needs.plan.outputs.publishing == 'true' }}
+    runs-on: ubuntu-latest
+    steps:
+      - name: print tag
+        run: echo "ok we're publishing!"
 
-  # Mark the Github Release™ as a non-draft now that everything has succeeded!
+  # Create a Github Release with all the results once everything is done,
   publish-release:
-    # Only run after all the other tasks, but it's ok if upload-artifacts was skipped
-    needs: [plan, upload-local-artifacts, upload-global-artifacts]
-    if: ${{ always() && needs.plan.result == 'success' && (needs.upload-local-artifacts.result == 'skipped' || needs.upload-local-artifacts.result == 'success') && (needs.upload-global-artifacts.result == 'skipped' || needs.upload-global-artifacts.result == 'success') }}
+    needs: [plan, should-publish]
     runs-on: ubuntu-latest
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -2289,8 +2318,18 @@ jobs:
       - uses: actions/checkout@v3
         with:
           submodules: recursive
-      - name: mark release as non-draft
-        run: |
-          gh release edit ${{ github.ref_name }} --draft=false
+      - name: "Download artifacts"
+        uses: actions/download-artifact@v3
+        with:
+          name: artifacts
+          path: artifacts
+      - name: Create Release
+        uses: ncipollo/release-action@v1
+        with:
+          tag: ${{ needs.plan.outputs.tag }}
+          name: ${{ fromJson(needs.plan.outputs.val).announcement_title }}
+          body: ${{ fromJson(needs.plan.outputs.val).announcement_github_body }}
+          prerelease: ${{ fromJson(needs.plan.outputs.val).announcement_is_prerelease }}
+          artifacts: "artifacts/*"
 
 
diff --git a/cargo-dist/tests/snapshots/install_path_cargo_home.snap b/cargo-dist/tests/snapshots/install_path_cargo_home.snap
index eccf4057..e00e5439 100644
--- a/cargo-dist/tests/snapshots/install_path_cargo_home.snap
+++ b/cargo-dist/tests/snapshots/install_path_cargo_home.snap
@@ -1237,6 +1237,35 @@ Install-Binary "$Args"
       "install_hint": "brew install axolotlsay",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }
 
diff --git a/cargo-dist/tests/snapshots/install_path_env_no_subdir.snap b/cargo-dist/tests/snapshots/install_path_env_no_subdir.snap
index 84b715ff..ef12f6e1 100644
--- a/cargo-dist/tests/snapshots/install_path_env_no_subdir.snap
+++ b/cargo-dist/tests/snapshots/install_path_env_no_subdir.snap
@@ -1217,6 +1217,35 @@ Install-Binary "$Args"
       "install_hint": "brew install axolotlsay",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }
 
diff --git a/cargo-dist/tests/snapshots/install_path_env_subdir.snap b/cargo-dist/tests/snapshots/install_path_env_subdir.snap
index 6b728217..221c186b 100644
--- a/cargo-dist/tests/snapshots/install_path_env_subdir.snap
+++ b/cargo-dist/tests/snapshots/install_path_env_subdir.snap
@@ -1217,6 +1217,35 @@ Install-Binary "$Args"
       "install_hint": "brew install axolotlsay",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }
 
diff --git a/cargo-dist/tests/snapshots/install_path_env_subdir_space.snap b/cargo-dist/tests/snapshots/install_path_env_subdir_space.snap
index de5852e1..faaa8a25 100644
--- a/cargo-dist/tests/snapshots/install_path_env_subdir_space.snap
+++ b/cargo-dist/tests/snapshots/install_path_env_subdir_space.snap
@@ -1217,6 +1217,35 @@ Install-Binary "$Args"
       "install_hint": "brew install axolotlsay",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }
 
diff --git a/cargo-dist/tests/snapshots/install_path_env_subdir_space_deeper.snap b/cargo-dist/tests/snapshots/install_path_env_subdir_space_deeper.snap
index 866aad28..f57b07ff 100644
--- a/cargo-dist/tests/snapshots/install_path_env_subdir_space_deeper.snap
+++ b/cargo-dist/tests/snapshots/install_path_env_subdir_space_deeper.snap
@@ -1217,6 +1217,35 @@ Install-Binary "$Args"
       "install_hint": "brew install axolotlsay",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }
 
diff --git a/cargo-dist/tests/snapshots/install_path_home_subdir_deeper.snap b/cargo-dist/tests/snapshots/install_path_home_subdir_deeper.snap
index 45ebf214..9caf1eeb 100644
--- a/cargo-dist/tests/snapshots/install_path_home_subdir_deeper.snap
+++ b/cargo-dist/tests/snapshots/install_path_home_subdir_deeper.snap
@@ -1217,6 +1217,35 @@ Install-Binary "$Args"
       "install_hint": "brew install axolotlsay",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }
 
diff --git a/cargo-dist/tests/snapshots/install_path_home_subdir_min.snap b/cargo-dist/tests/snapshots/install_path_home_subdir_min.snap
index a88e8d4c..9e513b06 100644
--- a/cargo-dist/tests/snapshots/install_path_home_subdir_min.snap
+++ b/cargo-dist/tests/snapshots/install_path_home_subdir_min.snap
@@ -1217,6 +1217,35 @@ Install-Binary "$Args"
       "install_hint": "brew install axolotlsay",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }
 
diff --git a/cargo-dist/tests/snapshots/install_path_home_subdir_space.snap b/cargo-dist/tests/snapshots/install_path_home_subdir_space.snap
index 11f27854..a411f0ea 100644
--- a/cargo-dist/tests/snapshots/install_path_home_subdir_space.snap
+++ b/cargo-dist/tests/snapshots/install_path_home_subdir_space.snap
@@ -1217,6 +1217,35 @@ Install-Binary "$Args"
       "install_hint": "brew install axolotlsay",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }
 
diff --git a/cargo-dist/tests/snapshots/install_path_home_subdir_space_deeper.snap b/cargo-dist/tests/snapshots/install_path_home_subdir_space_deeper.snap
index 69a1cbd1..754bea19 100644
--- a/cargo-dist/tests/snapshots/install_path_home_subdir_space_deeper.snap
+++ b/cargo-dist/tests/snapshots/install_path_home_subdir_space_deeper.snap
@@ -1217,6 +1217,35 @@ Install-Binary "$Args"
       "install_hint": "brew install axolotlsay",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }
 
diff --git a/cargo-dist/tests/snapshots/lib_manifest.snap b/cargo-dist/tests/snapshots/lib_manifest.snap
index 26928fcc..f8964522 100644
--- a/cargo-dist/tests/snapshots/lib_manifest.snap
+++ b/cargo-dist/tests/snapshots/lib_manifest.snap
@@ -11,6 +11,12 @@ stdout:
   "announcement_github_body": "CENSORED"
   "system_info": {
     "cargo_version_line": "CENSORED"
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {},
+      "pr_run_mode": "upload"
+    }
   }
 }
 
diff --git a/cargo-dist/tests/snapshots/lib_manifest_slash.snap b/cargo-dist/tests/snapshots/lib_manifest_slash.snap
index f5c6993c..91934bef 100644
--- a/cargo-dist/tests/snapshots/lib_manifest_slash.snap
+++ b/cargo-dist/tests/snapshots/lib_manifest_slash.snap
@@ -11,6 +11,12 @@ stdout:
   "announcement_github_body": "CENSORED"
   "system_info": {
     "cargo_version_line": "CENSORED"
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {},
+      "pr_run_mode": "upload"
+    }
   }
 }
 
diff --git a/cargo-dist/tests/snapshots/manifest.snap b/cargo-dist/tests/snapshots/manifest.snap
index d388fda0..84e9153b 100644
--- a/cargo-dist/tests/snapshots/manifest.snap
+++ b/cargo-dist/tests/snapshots/manifest.snap
@@ -231,6 +231,35 @@ stdout:
       "install_hint": "brew install axodotdev/homebrew-tap/cargo-dist",
       "description": "Install prebuilt binaries via Homebrew"
     }
+  },
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-FAKEVERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "runner": "macos-11",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-FAKEVERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "runner": "windows-2019",
+            "install_dist": "irm  https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-FAKEVERSION/cargo-dist-installer.ps1 | iex",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-FAKEVERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "upload"
+    }
   }
 }