This commit is contained in:
softprops 2019-08-25 02:13:05 -04:00
commit c1c36c7be2
9 changed files with 2129 additions and 0 deletions

32
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Main
on:
push:
branches:
- 'master'
jobs:
build:
runs-on: ubuntu-latest
steps:
# https://github.com/actions/checkout
- name: Checkout
uses: actions/checkout@master
with:
fetch-depth: 1
# https://github.com/actions/docker/tree/master/cli
- name: Package
uses: actions/docker/cli@master
with:
args: build -t ${{ github.repository }}:${{ github.sha }} .
# https://github.com/actions/docker/tree/master/login
- name: Publish Auth
uses: actions/docker/login@master
env:
# https://help.github.com/en/articles/virtual-environments-for-github-actions#creating-and-using-secrets-encrypted-variables
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
# - name: Publish
# uses: actions/docker/cli@master
# with:
# args: push ${{ github.repository }}:${{ github.sha }}

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
**/*.rs.bk

1854
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

20
Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "action-gh-release"
version = "0.1.0"
authors = ["softprops <d.tangren@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
mime = "0.3"
mime_guess = "2.0"
env_logger = "0.6"
log = "0.4"
glob = "0.3"
envy = "0.4"
# hack https://docs.rs/openssl/0.10.24/openssl/#vendored
openssl = { version = "0.10", features = ["vendored"] }
reqwest = { version = "0.9", features = ["rustls-tls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

42
Dockerfile Normal file
View file

@ -0,0 +1,42 @@
# https://hub.docker.com/_/rust?tab=tags
FROM rust:1.37.0 as builder
# musl-gcc
RUN apt-get update \
&& apt-get install -y \
musl-dev \
musl-tools \
ca-certificates \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN rustup target add x86_64-unknown-linux-musl
# cache deps https://blog.jawg.io/docker-multi-stage-build/
# RUN USER=root cargo init
# COPY Cargo.toml .
# RUN cargo build --target x86_64-unknown-linux-musl --release
# COPY src src
COPY . .
RUN cargo build --target x86_64-unknown-linux-musl --release \
&& strip /app/target/x86_64-unknown-linux-musl/release/action-gh-release
FROM scratch
# https://help.github.com/en/articles/metadata-syntax-for-github-actions#about-yaml-syntax-for-github-actions
LABEL version="0.1.0" \
repository="https://github.com/meetup/action-gh-release/" \
homepage="https://github.com/meetup/action-gh-release" \
maintainer="Meetup" \
"com.github.actions.name"="GH-Release" \
"com.github.actions.description"="Creates a new Github Release" \
"com.github.actions.icon"="package" \
"com.github.actions.color"="green"
COPY --from=builder \
/etc/ssl/certs/ca-certificates.crt \
/etc/ssl/certs/
COPY --from=builder \
/app/target/x86_64-unknown-linux-musl/release/action-gh-release \
/action-gh-release
ENTRYPOINT ["/action-gh-release"]

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# action gh-release
A Github Action for creating Github Releases

4
rustfmt.toml Normal file
View file

@ -0,0 +1,4 @@
# https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#fn_args_layout
fn_args_layout = "Vertical"
# https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#merge_imports
merge_imports = true

88
src/github.rs Normal file
View file

@ -0,0 +1,88 @@
use reqwest::{Body, Client};
use serde::{Deserialize, Serialize};
use std::{error::Error, fs::File};
#[derive(Serialize, Default, Debug, PartialEq)]
pub struct Release {
pub tag_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft: Option<bool>,
}
pub trait Releaser {
fn release(
&self,
github_token: &str,
github_repo: &str,
release: Release,
) -> Result<usize, Box<dyn Error>>;
}
pub trait AssetUploader<A: Into<Body> = File> {
fn upload(
&self,
github_token: &str,
github_repo: &str,
release_id: usize,
mime: mime::Mime,
asset: A,
) -> Result<(), Box<dyn Error>>;
}
#[derive(Deserialize)]
struct ReleaseResponse {
id: usize,
}
impl Releaser for Client {
// https://developer.github.com/v3/repos/releases/#create-a-release
fn release(
&self,
github_token: &str,
github_repo: &str,
release: Release,
) -> Result<usize, Box<dyn Error>> {
let response: ReleaseResponse = self
.post(&format!(
"https://api.github.com/repos/{}/releases",
github_repo
))
.header("Authorization", format!("bearer {}", github_token))
.json(&release)
.send()?
.json()?;
Ok(response.id)
}
}
impl<A: Into<Body>> AssetUploader<A> for Client {
// https://developer.github.com/v3/repos/releases/#upload-a-release-asset
fn upload(
&self,
github_token: &str,
github_repo: &str,
release_id: usize,
mime: mime::Mime,
asset: A,
) -> Result<(), Box<dyn Error>> {
self.post(&format!(
"http://uploads.github.com/repos/{}/releases/{}",
github_repo, release_id
))
.header("Authorization", format!("bearer {}", github_token))
.header("Content-Type", mime.to_string())
.body(asset)
.send()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {}
}

84
src/main.rs Normal file
View file

@ -0,0 +1,84 @@
mod github;
use github::{AssetUploader, Release, Releaser};
use reqwest::Client;
use serde::Deserialize;
use std::{error::Error, fs::File};
#[derive(Deserialize, Default)]
struct Config {
// provided
github_token: String,
github_ref: String, // refs/heads/..., ref/tags/...
github_repository: String,
// optional
input_body: Option<String>,
input_files: Option<Vec<String>>,
}
fn release(conf: &Config) -> Release {
let Config {
github_ref,
input_body,
..
} = conf;
Release {
tag_name: github_ref.clone(),
body: input_body.clone(),
..Release::default()
}
}
fn run(
conf: Config,
releaser: &dyn Releaser,
uploader: &dyn AssetUploader,
) -> Result<(), Box<dyn Error>> {
if !conf.github_ref.starts_with("refs/tags/") {
log::error!("GH Releases require a tag");
return Ok(());
}
let release_id = releaser.release(
conf.github_token.as_str(),
conf.github_repository.as_str(),
release(&conf),
)?;
if let Some(patterns) = conf.input_files {
for pattern in patterns {
for path in glob::glob(pattern.as_str())? {
let resolved = path?;
let mime =
mime_guess::from_path(&resolved).first_or(mime::APPLICATION_OCTET_STREAM);
uploader.upload(
conf.github_token.as_str(),
conf.github_repository.as_str(),
release_id,
mime,
File::open(resolved)?,
)?;
}
}
}
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
env_logger::init();
let client = Client::new();
run(envy::from_env()?, &client, &client)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn release_constructs_a_release_from_a_config() -> Result<(), Box<dyn Error>> {
for (conf, expect) in vec![(Config::default(), Release::default())] {
assert_eq!(release(&conf), expect);
}
Ok(())
}
}