diff --git a/.woodpecker/release.yml b/.woodpecker/release.yml new file mode 100644 index 0000000..d42e9a2 --- /dev/null +++ b/.woodpecker/release.yml @@ -0,0 +1,17 @@ +when: + - event: tag + +steps: + build: + image: rust:1 + commands: + - cargo run --release + + publish: + image: alpine:3.23 + commands: + - apk add --no-cache nodejs + - node ci/release.js target/release/sustenance + environment: + GITEA_RELEASE_TOKEN: + from_secret: gitea_release_token diff --git a/Cargo.toml b/Cargo.toml index d89c6fe..36a7649 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,9 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } url = "2" webauthn-rs = "0.3" + +[profile.release] +opt-level = "z" +strip = true +lto = true +codegen-units = 1 diff --git a/ci/release.js b/ci/release.js new file mode 100644 index 0000000..9118771 --- /dev/null +++ b/ci/release.js @@ -0,0 +1,91 @@ +import * as fs from 'node:fs/promises'; +import { basename } from 'node:path'; + +function getEnv(name) { + const val = process.env[name]; + if (!val) { + throw new Error(`Environment variable ${name} is empty`); + } + return val; +} + +async function fetchJSON(url, options) { + const resp = await fetch(url, options); + if (!resp.ok) { + throw new Error(`Unexpected HTTP status: ${resp.status}`, { + cause: { + status: resp.status, + body: await resp.text(), + }, + }); + } + return await resp.json(); +} + +async function postJSON(url, token, payload) { + return fetchJSON(url, { + method: "POST", + headers: { + "Authorization": `token ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); +} + +async function postFile(url, token, files) { + const formData = new FormData(); + for (const [name, path] of Object.entries(files)) { + const fileBuffer = await fs.readFile(path); + const fileObject = new File([fileBuffer], basename(path), { type: 'application/octet-stream' }); + formData.append(name, fileObject) + } + return await fetchJSON(url, { + method: "POST", + headers: { + "Authorization": `token ${token}`, + }, + body: formData, + }); +} + +async function canRead(path) { + try { + await fs.access(path, fs.constants.R_OK); + return true; + } catch { + return false; + } +} + +async function main() { + const path = process.argv[2]; + if (!path || !canRead(path)) { + throw Error(`Path ${path} is undefined or inaccessible, use node release.js `); + } + const token = getEnv("GITEA_RELEASE_TOKEN"); + const tag = getEnv("CI_COMMIT_TAG"); + const repo = getEnv("CI_REPO"); + + console.log("Creating release..."); + const releaseData = await postJSON(`https://git.sbstp.ca/api/v1/repos/${repo}/releases`, token, { + name: `Release ${tag}`, + tag_name: tag, + target_commitish: tag, + draft: false, + prerelease: false, + }); + console.log(`Created release ID ${releaseData.id}`); + + console.log("Uploading asset..."); + const assetData = await postFile(`https://git.sbstp.ca/api/v1/repos/${repo}/releases/${releaseData.id}/assets?name=${basename(path)}`, token, { + attachment: path, + }); + console.log("Asset uploaded:", assetData); +} + +try { + await main(); +} catch (err) { + console.error(err); +}