Artifacts: dsci@1c9fb55.1789570619. File: answer.md
**File structure**
```
.
├── .dsci
│ └── jobs.yaml
├── build
│ ├── config.yaml
│ ├── job.bash
│ └── tasks
│ ├── compile
│ │ └── task.bash
│ └── store_binary
│ └── task.bash
├── test
│ ├── config.yaml
│ ├── job.bash
│ └── tasks
│ ├── run_tests
│ │ └── task.bash
│ └── check_coverage
│ └── task.py
├── publish
│ ├── config.yaml
│ ├── job.bash
│ └── tasks
│ ├── docker_build
│ │ └── task.bash
│ └── docker_push
│ └── task.bash
└── Dockerfile # (assumed to exist for the Go binary)
```
---
## 1️⃣ Pipeline definition – **`.dsci/jobs.yaml`**
Only the list of jobs (and optional **input parameters**) is kept in the top‑level YAML.
```yaml
# .dsci/jobs.yaml
jobs:
-
id: build
path: build/
params:
binary_name: myapp # name of the compiled binary
-
id: test
path: test/
params:
coverage_threshold: 80 # required coverage % (fail if lower)
-
id: publish
path: publish/
params:
docker_registry: registry.example.com
image_name: myorg/myapp
image_tag: latest
```
*No job dependencies, matrices or third‑party actions – just a plain list of jobs.*
---
## 2️⃣ **Build** job – `build/`
### 2.1 Default parameters – `build/config.yaml`
```yaml
# build/config.yaml
binary_name: myapp # can be overridden by jobs.yaml params
```
### 2.2 Job entry point – `build/job.bash`
```bash
#!/usr/bin/env bash
# build/job.bash
# ------------------------------------------------------------------
# Build job – compiles the Go binary and stores it as an artifact.
# ------------------------------------------------------------------
# run the compile task
run_task compile
# store the resulting binary in the shared artifacts directory
run_task store_binary
```
### 2.3 Tasks
#### `build/tasks/compile/task.bash`
```bash
#!/usr/bin/env bash
# build/tasks/compile/task.bash
# Compile the Go binary
set -euo pipefail
# read the binary name (job parameter)
binary_name=$(config binary_name)
# build the binary (assumes Go modules are present at repo root)
echo "🔨 Building Go binary '${binary_name}'..."
go build -o "${binary_name}" ./...
# export the binary path as a state so downstream jobs can read it
update_state "{ \"binary_path\": \"${PWD}/${binary_name}\" }"
```
#### `build/tasks/store_binary/task.bash`
```bash
#!/usr/bin/env bash
# build/tasks/store_binary/task.bash
# Copy the binary to the DSCI artifacts directory (~/artifacts)
set -euo pipefail
# get the path from the state set by the previous task
binary_path=$(get_state | jq -r '._dsci_.build.binary_path')
echo "📦 Storing binary at ~/artifacts/${binary_path##*/}"
mkdir -p ~/artifacts
cp "${binary_path}" ~/artifacts/
```
---
## 3️⃣ **Test** job – `test/`
### 3.1 Default parameters – `test/config.yaml`
```yaml
# test/config.yaml
coverage_threshold: 80 # % – can be overridden in jobs.yaml
```
### 3.2 Job entry point – `test/job.bash`
```bash
#!/usr/bin/env bash
# test/job.bash
# ------------------------------------------------------------------
# Test job – runs `go test` with coverage, checks the threshold.
# ------------------------------------------------------------------
# execute the unit‑test task
run_task run_tests
# evaluate the coverage percentage
run_task check_coverage
```
### 3.3 Tasks
#### `test/tasks/run_tests/task.bash`
```bash
#!/usr/bin/env bash
# test/tasks/run_tests/task.bash
# Run Go tests and generate a coverage profile
set -euo pipefail
COVERAGE_FILE="coverage.out"
echo "🧪 Running unit tests with coverage..."
go test ./... -coverprofile="${COVERAGE_FILE}" -covermode=atomic
# store the coverage file as an artifact (shared with the next task)
mkdir -p ~/artifacts
cp "${COVERAGE_FILE}" ~/artifacts/
# also expose the path via state (optional)
update_state "{ \"coverage_file\": \"${PWD}/${COVERAGE_FILE}\" }"
```
#### `test/tasks/check_coverage/task.py`
```python
#!/usr/bin/env python3
# test/tasks/check_coverage/task.py
# Parse the coverage report, compare with the required threshold,
# and fail the job if the threshold is not met.
import subprocess
import sys
import json
from pathlib import Path
# ----------------------------------------------------------------------
# Helper to read DSCI configuration / state
# ----------------------------------------------------------------------
def config():
"""Return the merged job configuration (params + defaults)."""
# DSCI injects a `config` function at runtime; we just call it.
# The implementation is provided by the DSCI runtime.
...
def get_state():
"""Return the current job state as a Python dict."""
...
# ----------------------------------------------------------------------
# Main logic
# ----------------------------------------------------------------------
def main():
cfg = config()
threshold = float(cfg.get("coverage_threshold", 0))
# locate the coverage file (saved as an artifact by previous task)
coverage_path = Path.home() / "artifacts" / "coverage.out"
if not coverage_path.is_file():
print(f"❌ Coverage file not found at {coverage_path}", file=sys.stderr)
sys.exit(1)
# Use `go tool cover -func` to get a line like:
# total: (statements) 85.3%
result = subprocess.run(
["go", "tool", "cover", "-func", str(coverage_path)],
capture_output=True,
text=True,
check=True,
)
# Extract the total coverage percentage
total_line = next(
(ln for ln in result.stdout.splitlines() if ln.startswith("total:")), None
)
if not total_line:
print("❌ Could not find total coverage line.", file=sys.stderr)
sys.exit(1)
coverage_percent = float(total_line.split()[-1].replace("%", ""))
print(f"📈 Total coverage: {coverage_percent:.2f}% (threshold: {threshold}%)")
# Save the percentage in the state for downstream jobs (optional)
update_state(json.dumps({"coverage_percent": coverage_percent}))
if coverage_percent < threshold:
print(
f"❌ Coverage {coverage_percent:.2f}% is below the required {threshold}%!",
file=sys.stderr,
)
sys.exit(1)
print("✅ Coverage threshold satisfied.")
if __name__ == "__main__":
main()
```
---
## 4️⃣ **Publish** job – `publish/`
### 4.1 Default parameters – `publish/config.yaml`
```yaml
# publish/config.yaml
docker_registry: registry.example.com
image_name: myorg/myapp
image_tag: latest
```
### 4.2 Job entry point – `publish/job.bash`
```bash
#!/usr/bin/env bash
# publish/job.bash
# ------------------------------------------------------------------
# Publish job – builds a Docker image from the compiled binary and
# pushes it to a registry.
# ------------------------------------------------------------------
# Build the Docker image
run_task docker_build
# Push the Docker image
run_task docker_push
```
### 4.3 Tasks
#### `publish/tasks/docker_build/task.bash`
```bash
#!/usr/bin/env bash
# publish/tasks/docker_build/task.bash
# Build a Docker image that contains the compiled Go binary.
set -euo pipefail
# ------------------------------------------------------------------
# 1. Resolve configuration
# ------------------------------------------------------------------
binary_name=$(config binary_name) # comes from build job defaults
registry=$(config docker_registry)
image_name=$(config image_name)
image_tag=$(config image_tag)
# ------------------------------------------------------------------
# 2. Locate the binary saved as an artifact by the build job
# ------------------------------------------------------------------
binary_path=$(get_state | jq -r '._dsci_.build.binary_path // ""')
if [[ -z "$binary_path" ]]; then
# fallback – the binary should also be present in ~/artifacts
binary_path="${HOME}/artifacts/${binary_name}"
fi
if [[ ! -f "$binary_path" ]]; then
echo "❌ Compiled binary not found at $binary_path"
exit 1
fi
# ------------------------------------------------------------------
# 3. Build the Docker image
# ------------------------------------------------------------------
full_image="${registry}/${image_name}:${image_tag}"
echo "🐳 Building Docker image ${full_image}..."
# We copy the binary into the current workdir because Dockerfile
# expects a file called ${binary_name}
cp "${binary_path}" "./${binary_name}"
docker build \
--pull \
-t "${full_image}" \
--build-arg BINARY_NAME="${binary_name}" \
.
# expose image name via state (optional, useful for downstream jobs)
update_state "{ \"docker_image\": \"${full_image}\" }"
```
> **Dockerfile example** (must exist at repository root)
```Dockerfile
# Dockerfile (used by the publish job)
FROM alpine:3.20 AS runtime
ARG BINARY_NAME
COPY ${BINARY_NAME} /usr/local/bin/${BINARY_NAME}
RUN chmod +x /usr/local/bin/${BINARY_NAME}
ENTRYPOINT ["/usr/local/bin/${BINARY_NAME}"]
```
#### `publish/tasks/docker_push/task.bash`
```bash
#!/usr/bin/env bash
# publish/tasks/docker_push/task.bash
# Push the Docker image built in the previous task.
set -euo pipefail
registry=$(config docker_registry)
image_name=$(config image_name)
image_tag=$(config image_tag)
full_image="${registry}/${image_name}:${image_tag}"
echo "📤 Pushing Docker image ${full_image} to registry ${registry}..."
# Login – assumes the runner provides DOCKER_USERNAME / DOCKER_PASSWORD env vars
if [[ -n "${DOCKER_USERNAME:-}" && -n "${DOCKER_PASSWORD:-}" ]]; then
echo "${DOCKER_PASSWORD}" | docker login "${registry}" -u "${DOCKER_USERNAME}" --password-stdin
fi
docker push "${full_image}"
echo "✅ Image pushed successfully."
```
---
## 5️⃣ How the pieces talk together
| Job / Task | What it does | How data is passed |
|------------|--------------|-------------------|
| **build → compile** | `go build -o