Artifacts: dsci@1c9fb55.1789570619. File: answer.md


artifacts

**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 ` | `update_state` → `binary_path` | | **build → store_binary** | Copies binary to `~/artifacts/` | Binary is now a file that any later job can read | | **test → run_tests** | `go test -coverprofile=coverage.out` | Writes `coverage.out` to `~/artifacts/` | | **test → check_coverage** | Python parses `coverage.out`, compares with `coverage_threshold` (from `config()`), `update_state` → `coverage_percent` | | **publish → docker_build** | Reads binary from `~/artifacts/` (or from state), builds Docker image, `update_state` → `docker_image` | | **publish → docker_push** |