This commit is contained in:
1 parent
9f03384bc9
commit
f5f0d3999e
30 files changed
+96
-93
No files matched your search
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "herald",
|
||||
"name": "steep",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"args": {
|
||||
@@ -17,8 +17,8 @@
|
||||
"extensions": ["fill-labs.dependi", "rust-lang.rust-analyzer", "tamasfe.even-better-toml"]
|
||||
}
|
||||
},
|
||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/herald,type=bind",
|
||||
"workspaceFolder": "/workspaces/herald",
|
||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/steep,type=bind",
|
||||
"workspaceFolder": "/workspaces/steep",
|
||||
"runArgs": ["--userns=keep-id", "--security-opt", "label=disable"],
|
||||
"appPort": [3000]
|
||||
}
|
||||
@@ -26,4 +26,4 @@ steps:
|
||||
volumes:
|
||||
- /data/woodpecker-builds:/data
|
||||
commands:
|
||||
- buildah bud -f Containerfile -t herald-ci .
|
||||
- buildah bud -f Containerfile -t steep-ci .
|
||||
Generated
+28
-28
@@ -766,34 +766,6 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "herald-server"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"devcontainer-rs",
|
||||
"dotenvy",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"metrics",
|
||||
"metrics-exporter-prometheus",
|
||||
"reqwest",
|
||||
"ring",
|
||||
"sentry",
|
||||
"sentry-anyhow",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http 0.7.1",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
@@ -2236,6 +2208,34 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "steep-server"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"devcontainer-rs",
|
||||
"dotenvy",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"metrics",
|
||||
"metrics-exporter-prometheus",
|
||||
"reqwest",
|
||||
"ring",
|
||||
"sentry",
|
||||
"sentry-anyhow",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http 0.7.1",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/herald-server",
|
||||
"crates/steep-server",
|
||||
"crates/devcontainer-rs",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
+4
-4
@@ -5,14 +5,14 @@ WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/ crates/
|
||||
|
||||
RUN cargo build --release --package herald-server
|
||||
RUN cargo build --release --package steep-server
|
||||
|
||||
|
||||
FROM debian:trixie-slim
|
||||
|
||||
# git clones the pull request. ca-certificates is what every HTTPS call needs
|
||||
# (Gitea, OpenRouter, git). The shared libraries are the non-base ones the binary
|
||||
# links against, as reported by `ldd target/release/herald-server`; libc, libm
|
||||
# links against, as reported by `ldd target/release/steep-server`; libc, libm
|
||||
# and libgcc_s come from the base image.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
@@ -24,8 +24,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/target/release/herald-server ./herald-server
|
||||
COPY --from=builder /app/target/release/steep-server ./steep-server
|
||||
|
||||
# Exec form, so the binary is PID 1 and receives the SIGTERM it handles to shut
|
||||
# down gracefully.
|
||||
CMD ["./herald-server"]
|
||||
CMD ["./steep-server"]
|
||||
@@ -1,6 +1,10 @@
|
||||
# Herald
|
||||

|
||||
|
||||
Herald is a Gitea bot that performs automated AI-powered code reviews on pull requests using [OpenRouter](https://openrouter.ai/).
|
||||
# Steep
|
||||
|
||||
*Let your code steep.*
|
||||
|
||||
Steep is a Gitea bot that performs automated AI-powered code reviews on pull requests using [OpenRouter](https://openrouter.ai/).
|
||||
|
||||
## Features
|
||||
|
||||
@@ -10,18 +14,18 @@ Herald is a Gitea bot that performs automated AI-powered code reviews on pull re
|
||||
- Graceful shutdown — in-progress reviews finish before the process exits
|
||||
- Error tracking via Sentry
|
||||
- Prometheus metrics endpoint for monitoring
|
||||
- Tiny memory footprint (~4MB) thanks to Rust
|
||||
- Lightweight, performant Rust service with Prometheus metrics and Sentry error tracking
|
||||
|
||||
## Installation
|
||||
|
||||
**Requirements:** Rust toolchain ([rustup.rs](https://rustup.rs))
|
||||
|
||||
```sh
|
||||
cargo build --release
|
||||
./target/release/herald
|
||||
cargo build --release -p steep-server
|
||||
./target/release/steep-server
|
||||
```
|
||||
|
||||
Herald reads its configuration from environment variables (a `.env` file is supported):
|
||||
Steep reads its configuration from environment variables (a `.env` file is supported):
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
@@ -38,11 +42,11 @@ Herald reads its configuration from environment variables (a `.env` file is supp
|
||||
| `SENTRY_DSN` | *(optional)* Sentry DSN for error tracking |
|
||||
| `RUST_LOG` | *(optional)* Log level, defaults to `info` |
|
||||
| `SANDBOX_MAX_ITERATIONS` | *(optional)* Maximum number of tool-calling iterations per sandboxed review. Defaults to `8` |
|
||||
| `DOCKER_HOST` | *(optional)* Container daemon socket Herald drives, e.g. `unix:///run/user/1000/podman/podman.sock` for a rootless podman. Defaults to `unix:///var/run/docker.sock` |
|
||||
| `DOCKER_HOST` | *(optional)* Container daemon socket Steep drives, e.g. `unix:///run/user/1000/podman/podman.sock` for a rootless podman. Defaults to `unix:///var/run/docker.sock` |
|
||||
|
||||
## Sandboxed reviews
|
||||
|
||||
Herald reviews pull requests inside an ephemeral
|
||||
Steep reviews pull requests inside an ephemeral
|
||||
[Dev Container](https://containers.dev/). For each review it:
|
||||
|
||||
1. clones the pull request head into a temporary directory,
|
||||
@@ -66,14 +70,14 @@ Each comment is tagged with a severity — `bug`, `security`, `performance` or
|
||||
`maintainability` — shown at the start of the comment, and the summary also lists
|
||||
what the pull request does well.
|
||||
|
||||
Herald drives the container daemon through its socket: `DOCKER_HOST` (default
|
||||
Steep drives the container daemon through its socket: `DOCKER_HOST` (default
|
||||
`unix:///var/run/docker.sock`), which covers both docker and podman's
|
||||
Docker-compatible socket. When the repository contains a
|
||||
`.devcontainer/devcontainer.json` (or `.devcontainer.json`), Herald uses it;
|
||||
`.devcontainer/devcontainer.json` (or `.devcontainer.json`), Steep uses it;
|
||||
otherwise it falls back to a default devcontainer that pulls `debian:stable-slim`
|
||||
and runs the review in `/workspace`.
|
||||
|
||||
Herald can therefore run inside a container with only that socket mounted (no
|
||||
Steep can therefore run inside a container with only that socket mounted (no
|
||||
shared workspace directory is required): the clone is streamed to the daemon over
|
||||
the socket, like the build context, instead of being bind-mounted from a host
|
||||
path the daemon would have to see. This is the setup the `Containerfile`
|
||||
@@ -82,7 +86,7 @@ produces, e.g.:
|
||||
```sh
|
||||
podman run --env-file=.env -p 3001:3001 \
|
||||
-v /run/user/$(id -u)/podman/podman.sock:/var/run/docker.sock \
|
||||
herald:latest
|
||||
steep:latest
|
||||
```
|
||||
|
||||
The `runArgs` of that file are ignored: they come from an untrusted pull request,
|
||||
@@ -107,28 +111,28 @@ Open the project and reopen it in the container — the Rust toolchain is pre-in
|
||||
|
||||
```sh
|
||||
rustup toolchain install stable
|
||||
cargo run
|
||||
cargo run -p steep-server
|
||||
```
|
||||
|
||||
Copy `.env.example` to `.env` and fill in your values before running.
|
||||
|
||||
## Metrics
|
||||
|
||||
Herald optionally exposes a Prometheus metrics endpoint, useful for scraping with an [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) or any Prometheus-compatible scraper.
|
||||
Steep optionally exposes a Prometheus metrics endpoint, useful for scraping with an [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) or any Prometheus-compatible scraper.
|
||||
|
||||
Set `METRICS_BIND_ADDR` (e.g. `0.0.0.0:9100`) to enable it. The metrics are then available at `http://<host>:9100/metrics`.
|
||||
Set `METRICS_BIND_ADDR` (e.g. `0.0.0.0:9100`) to enable it. The metrics are then available at `http://<host>:9100/metrics`. The metric namespace changed from `herald_*` to `steep_*`; update any dashboards or alerts that use the old names.
|
||||
|
||||
### Exposed metrics
|
||||
|
||||
| Metric | Type | Description |
|
||||
|---|---|---|
|
||||
| `herald_webhooks_received_total` | counter | Total webhooks received (label: `event_type`) |
|
||||
| `herald_webhooks_duplicate_total` | counter | Webhooks rejected as duplicates (label: `event_type`) |
|
||||
| `herald_webhooks_channel_full_total` | counter | Webhooks dropped because the bot channel was full (label: `event_type`) |
|
||||
| `herald_bot_tasks_active` | gauge | Bot tasks currently in progress |
|
||||
| `herald_bot_tasks_completed_total` | counter | Bot tasks completed successfully (label: `event_type`) |
|
||||
| `herald_bot_tasks_failed_total` | counter | Bot tasks that failed (label: `event_type`) |
|
||||
| `herald_openrouter_cost_cents_total` | counter | Total OpenRouter cost in cents (divide by 100 for USD) |
|
||||
| `steep_webhooks_received_total` | counter | Total webhooks received (label: `event_type`) |
|
||||
| `steep_webhooks_duplicate_total` | counter | Webhooks rejected as duplicates (label: `event_type`) |
|
||||
| `steep_webhooks_channel_full_total` | counter | Webhooks dropped because the bot channel was full (label: `event_type`) |
|
||||
| `steep_bot_tasks_active` | gauge | Bot tasks currently in progress |
|
||||
| `steep_bot_tasks_completed_total` | counter | Bot tasks completed successfully (label: `event_type`) |
|
||||
| `steep_bot_tasks_failed_total` | counter | Bot tasks that failed (label: `event_type`) |
|
||||
| `steep_openrouter_cost_cents_total` | counter | Total OpenRouter cost in cents (divide by 100 for USD) |
|
||||
|
||||
### OTel collector example
|
||||
|
||||
@@ -137,10 +141,10 @@ receivers:
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
- job_name: herald
|
||||
- job_name: steep
|
||||
scrape_interval: 15s
|
||||
static_configs:
|
||||
- targets: ["herald:9100"]
|
||||
- targets: ["steep:9100"]
|
||||
|
||||
service:
|
||||
pipelines:
|
||||
|
||||
+3
-3
@@ -13,9 +13,9 @@
|
||||
|
||||
## 1.3 — Typed action routing — planned
|
||||
|
||||
- Introduce a type-safe model output for the action Herald should take, such as `Review` or `Implementation`.
|
||||
- Route review requests (for example, “@herald, review this PR”) to the existing review flow.
|
||||
- Recognize implementation requests (for example, “@herald, rename method `XXX` to `YYY`”) and establish their dispatch path. Execution of implementation requests is planned for 2.0.
|
||||
- Introduce a type-safe model output for the action Steep should take, such as `Review` or `Implementation`.
|
||||
- Route review requests (for example, “@steep, review this PR”) to the existing review flow.
|
||||
- Recognize implementation requests (for example, “@steep, rename method `XXX` to `YYY`”) and establish their dispatch path. Execution of implementation requests is planned for 2.0.
|
||||
|
||||
## 2.0 — Implementation requests — planned
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1009 KiB |
@@ -7,7 +7,7 @@
|
||||
//! l'intérieur du container en cours d'exécution.
|
||||
//!
|
||||
//! Le daemon est joint par son **socket** (celui de l'hôte, monté dans le
|
||||
//! container de Herald) : [`ContainerRuntime::connect`] suit `DOCKER_HOST` comme
|
||||
//! container de Steep) : [`ContainerRuntime::connect`] suit `DOCKER_HOST` comme
|
||||
//! le fait le CLI `docker`, et retombe sur le socket local. Aucun binaire de
|
||||
//! runtime n'est donc requis dans l'image, et `podman` fonctionne de la même
|
||||
//! façon dès lors qu'il expose son socket compatible Docker.
|
||||
|
||||
@@ -112,7 +112,7 @@ impl TryFrom<(DevContainerSchema, PathBuf)> for DevContainer {
|
||||
/// La spécification veut que `${localEnv:VAR}` soit lu dans l'environnement du
|
||||
/// client ; ici le `devcontainer.json` vient d'une **pull request**, donc de code
|
||||
/// non fiable : lire l'environnement du processus lui permettrait de récupérer
|
||||
/// `GITEA_TOKEN`, `OPEN_ROUTER_API_KEY` ou n'importe quel autre secret de Herald et
|
||||
/// `GITEA_TOKEN`, `OPEN_ROUTER_API_KEY` ou n'importe quel autre secret de Steep et
|
||||
/// de l'exfiltrer depuis un `build.args`, un `containerEnv`, son Dockerfile ou un
|
||||
/// hook. L'environnement n'est donc **jamais** consulté : seule la valeur par
|
||||
/// défaut est utilisée, et une référence sans défaut devient une chaîne vide.
|
||||
@@ -272,7 +272,7 @@ mod tests {
|
||||
unsafe { std::env::set_var("DEVCONTAINER_TEST_SECRET", "s3cret") };
|
||||
|
||||
// Le `devcontainer.json` vient d'une pull request : résoudre la référence
|
||||
// depuis l'environnement de Herald y exposerait ses jetons.
|
||||
// depuis l'environnement de Steep y exposerait ses jetons.
|
||||
assert_eq!(
|
||||
substitute_local_env("${localEnv:DEVCONTAINER_TEST_SECRET}"),
|
||||
""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "herald-server"
|
||||
name = "steep-server"
|
||||
version = "1.2.0"
|
||||
edition = "2024"
|
||||
|
||||
@@ -47,7 +47,7 @@ pub async fn start(app_state: AppState, shutdown: CancellationToken) -> anyhow::
|
||||
}
|
||||
|
||||
async fn root() -> &'static str {
|
||||
"Hi, i'm Herald :)"
|
||||
"Hi, I'm Steep :)"
|
||||
}
|
||||
|
||||
#[instrument(skip(app_state), fields(webhook_type), err)]
|
||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -875,7 +875,7 @@ mod tests {
|
||||
#[test]
|
||||
fn generated_lockfiles_are_ignored() {
|
||||
assert!(is_ignored("Cargo.lock"));
|
||||
assert!(is_ignored("crates/herald-server/Cargo.lock"));
|
||||
assert!(is_ignored("crates/steep-server/Cargo.lock"));
|
||||
assert!(is_ignored("package-lock.json"));
|
||||
assert!(is_ignored("web/yarn.lock"));
|
||||
assert!(is_ignored("go.sum"));
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -76,7 +76,7 @@ async fn run() -> anyhow::Result<()> {
|
||||
model = %config.open_router_model,
|
||||
gitea_url = %config.gitea_url,
|
||||
bot_name = %gitea_user.login,
|
||||
"Starting Herald"
|
||||
"Starting Steep"
|
||||
);
|
||||
|
||||
let open_router_client = OpenRouterClient::new(
|
||||
@@ -3,73 +3,72 @@ use std::{net::SocketAddr, str::FromStr};
|
||||
use metrics::{Unit, counter, describe_counter, describe_gauge, gauge};
|
||||
|
||||
pub fn webhook_received(event_type: &str) {
|
||||
counter!("herald_webhooks_received_total", "event_type" => event_type.to_string()).increment(1);
|
||||
counter!("steep_webhooks_received_total", "event_type" => event_type.to_string()).increment(1);
|
||||
}
|
||||
|
||||
pub fn webhook_duplicate(event_type: &str) {
|
||||
counter!("herald_webhooks_duplicate_total", "event_type" => event_type.to_string())
|
||||
.increment(1);
|
||||
counter!("steep_webhooks_duplicate_total", "event_type" => event_type.to_string()).increment(1);
|
||||
}
|
||||
|
||||
pub fn webhook_channel_full(event_type: &str) {
|
||||
counter!("herald_webhooks_channel_full_total", "event_type" => event_type.to_string())
|
||||
counter!("steep_webhooks_channel_full_total", "event_type" => event_type.to_string())
|
||||
.increment(1);
|
||||
}
|
||||
|
||||
pub fn increment_task_active() {
|
||||
gauge!("herald_bot_tasks_active").increment(1.0);
|
||||
gauge!("steep_bot_tasks_active").increment(1.0);
|
||||
}
|
||||
|
||||
pub fn decrement_task_active() {
|
||||
gauge!("herald_bot_tasks_active").decrement(1.0);
|
||||
gauge!("steep_bot_tasks_active").decrement(1.0);
|
||||
}
|
||||
|
||||
pub fn task_completed(event_type: &str) {
|
||||
counter!("herald_bot_tasks_completed_total", "event_type" => event_type.to_string())
|
||||
counter!("steep_bot_tasks_completed_total", "event_type" => event_type.to_string())
|
||||
.increment(1);
|
||||
}
|
||||
|
||||
pub fn task_failed(event_type: &str) {
|
||||
counter!("herald_bot_tasks_failed_total", "event_type" => event_type.to_string()).increment(1);
|
||||
counter!("steep_bot_tasks_failed_total", "event_type" => event_type.to_string()).increment(1);
|
||||
}
|
||||
|
||||
pub fn openrouter_cost_usd(cost: f64) {
|
||||
counter!("herald_openrouter_cost_cents_total").increment((cost * 100.0).round() as u64);
|
||||
counter!("steep_openrouter_cost_cents_total").increment((cost * 100.0).round() as u64);
|
||||
}
|
||||
|
||||
pub fn describe() {
|
||||
describe_counter!(
|
||||
"herald_webhooks_received_total",
|
||||
"steep_webhooks_received_total",
|
||||
Unit::Count,
|
||||
"Total webhooks received"
|
||||
);
|
||||
describe_counter!(
|
||||
"herald_webhooks_duplicate_total",
|
||||
"steep_webhooks_duplicate_total",
|
||||
Unit::Count,
|
||||
"Webhooks rejected as duplicates"
|
||||
);
|
||||
describe_counter!(
|
||||
"herald_webhooks_channel_full_total",
|
||||
"steep_webhooks_channel_full_total",
|
||||
Unit::Count,
|
||||
"Webhooks dropped because the bot channel was full"
|
||||
);
|
||||
describe_gauge!(
|
||||
"herald_bot_tasks_active",
|
||||
"steep_bot_tasks_active",
|
||||
Unit::Count,
|
||||
"Bot tasks currently in progress"
|
||||
);
|
||||
describe_counter!(
|
||||
"herald_bot_tasks_completed_total",
|
||||
"steep_bot_tasks_completed_total",
|
||||
Unit::Count,
|
||||
"Bot tasks completed successfully"
|
||||
);
|
||||
describe_counter!(
|
||||
"herald_bot_tasks_failed_total",
|
||||
"steep_bot_tasks_failed_total",
|
||||
Unit::Count,
|
||||
"Bot tasks that failed"
|
||||
);
|
||||
describe_counter!(
|
||||
"herald_openrouter_cost_cents_total",
|
||||
"steep_openrouter_cost_cents_total",
|
||||
Unit::Count,
|
||||
"Total OpenRouter cost in cents (divide by 100 for USD)"
|
||||
);
|
||||
@@ -1,10 +1,10 @@
|
||||
//! Minimal OpenRouter chat-completions client.
|
||||
//!
|
||||
//! Herald only needs a non-streaming `POST /chat/completions` with optional
|
||||
//! Steep only needs a non-streaming `POST /chat/completions` with optional
|
||||
//! tool calling, so the wire types are implemented in-tree instead of pulling a
|
||||
//! third-party SDK (and its own `reqwest` version) into the workspace.
|
||||
//!
|
||||
//! Only the response fields Herald consumes are modelled: `content`,
|
||||
//! Only the response fields Steep consumes are modelled: `content`,
|
||||
//! `tool_calls` and `usage.cost`. Unknown fields are ignored.
|
||||
|
||||
use std::time::Duration;
|
||||
@@ -174,7 +174,7 @@ struct ChatRequest<'a> {
|
||||
tool_choice: &'a str,
|
||||
}
|
||||
|
||||
/// OpenRouter reasoning config; Herald always asks for reasoning.
|
||||
/// OpenRouter reasoning config; Steep always asks for reasoning.
|
||||
#[derive(Serialize)]
|
||||
struct Reasoning {
|
||||
enabled: bool,
|
||||
File renamed without changes.
+2
-2
@@ -214,10 +214,10 @@ async fn clone_pull_request(
|
||||
|
||||
/// Rend le clone lisible par les utilisateurs du container sandbox.
|
||||
///
|
||||
/// Le container peut ne pas avoir les mêmes uid que Herald (podman rootless
|
||||
/// Le container peut ne pas avoir les mêmes uid que Steep (podman rootless
|
||||
/// mappe les uid à travers des plages subuid), et l'umask de l'opérateur peut être
|
||||
/// restrictif : sans cela, les outils de la sandbox échouent en `Permission
|
||||
/// denied` sur des fichiers que Herald vient de cloner lui-même.
|
||||
/// denied` sur des fichiers que Steep vient de cloner lui-même.
|
||||
async fn make_readable(repo_dir: &Path) -> anyhow::Result<()> {
|
||||
let output = tokio::process::Command::new("chmod")
|
||||
.args(["-R", "a+rX"])
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.
|
After Width: | Height: | Size: 843 KiB |
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE="tintounn/herald"
|
||||
IMAGE="tintounn/steep"
|
||||
|
||||
if [ -z "${CI_COMMIT_TAG:-}" ]; then
|
||||
echo "Error: CI_COMMIT_TAG is not set" >&2
|
||||
|
||||
Reference in new issue
Block a user