From ce0dbf6b249956700c6a1705bf4ad85a09d53e8c Mon Sep 17 00:00:00 2001 From: Dawid Rycerz Date: Sun, 15 Feb 2026 21:27:00 +0100 Subject: feat: witryna 0.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch, cleanup, and status CLI commands. Persistent build state via state.json. Post-deploy hooks on success and failure with WITRYNA_BUILD_STATUS. Dependency diet (axum→tiny_http, clap→argh, tracing→log). Drop built-in rate limiting. Nix flake with NixOS module. Arch Linux PKGBUILD. Centralized version management. Co-Authored-By: Claude Opus 4.6 --- tests/integration/hooks.rs | 148 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) (limited to 'tests/integration/hooks.rs') diff --git a/tests/integration/hooks.rs b/tests/integration/hooks.rs index 86684cc..d8b4fa3 100644 --- a/tests/integration/hooks.rs +++ b/tests/integration/hooks.rs @@ -1,6 +1,7 @@ use crate::git_helpers::create_local_repo; use crate::harness::{SiteBuilder, TestServer, test_config_with_site}; use crate::runtime::{skip_without_git, skip_without_runtime}; +use std::path::Path; use std::time::Duration; // --------------------------------------------------------------------------- @@ -135,3 +136,150 @@ async fn post_deploy_hook_failure_nonfatal() { } assert!(found_hook_log, "hook log should exist for failed hook"); } + +#[tokio::test] +async fn post_deploy_hook_runs_on_build_failure() { + skip_without_git!(); + skip_without_runtime!(); + + let tempdir = tempfile::tempdir().unwrap(); + let base_dir = tempdir.path().to_path_buf(); + + let repo_dir = tempdir.path().join("repos"); + tokio::fs::create_dir_all(&repo_dir).await.unwrap(); + let repo_url = create_local_repo(&repo_dir, "main").await; + + // Build command fails (exit 1); hook writes WITRYNA_BUILD_STATUS to a file in clone dir + let site = SiteBuilder::new("hook-on-fail", &repo_url, "test-token") + .overrides("alpine:latest", "exit 1", "out") + .post_deploy(vec![ + "sh".to_owned(), + "-c".to_owned(), + "echo \"$WITRYNA_BUILD_STATUS\" > hook-status.txt".to_owned(), + ]) + .build(); + + let server = TestServer::start(test_config_with_site(base_dir.clone(), site)).await; + + let resp = TestServer::client() + .post(server.url("/hook-on-fail")) + .header("Authorization", "Bearer test-token") + .send() + .await + .unwrap(); + assert_eq!(resp.status().as_u16(), 202); + + // Wait for state.json to show "failed" (no current symlink on build failure) + let state_path = base_dir.join("builds/hook-on-fail/state.json"); + let max_wait = Duration::from_secs(120); + let start = std::time::Instant::now(); + + loop { + assert!(start.elapsed() <= max_wait, "build timed out"); + if state_path.exists() { + let content = tokio::fs::read_to_string(&state_path) + .await + .unwrap_or_default(); + if content.contains("\"failed\"") { + // Give the hook a moment to finish writing + tokio::time::sleep(Duration::from_secs(2)).await; + break; + } + } + tokio::time::sleep(Duration::from_millis(500)).await; + } + + // Verify hook ran and received build_status=failed + let clone_dir = base_dir.join("clones/hook-on-fail"); + let hook_status_path = clone_dir.join("hook-status.txt"); + assert!( + hook_status_path.exists(), + "hook should have created hook-status.txt in clone dir" + ); + let status = tokio::fs::read_to_string(&hook_status_path).await.unwrap(); + assert_eq!( + status.trim(), + "failed", + "hook should receive build_status=failed" + ); + + // Verify state.json says "failed" (not "hook failed") + let state_content = tokio::fs::read_to_string(&state_path).await.unwrap(); + assert!( + state_content.contains("\"failed\""), + "state.json should show failed status" + ); + + // No current symlink should exist (build failed) + assert!( + !Path::new(&base_dir.join("builds/hook-on-fail/current")).is_symlink(), + "current symlink should not exist on build failure" + ); +} + +#[tokio::test] +async fn post_deploy_hook_receives_success_status() { + skip_without_git!(); + skip_without_runtime!(); + + let tempdir = tempfile::tempdir().unwrap(); + let base_dir = tempdir.path().to_path_buf(); + + let repo_dir = tempdir.path().join("repos"); + tokio::fs::create_dir_all(&repo_dir).await.unwrap(); + let repo_url = create_local_repo(&repo_dir, "main").await; + + // Successful build; hook writes WITRYNA_BUILD_STATUS to build dir + let site = SiteBuilder::new("hook-success-status", &repo_url, "test-token") + .overrides( + "alpine:latest", + "mkdir -p out && echo test > out/index.html", + "out", + ) + .post_deploy(vec![ + "sh".to_owned(), + "-c".to_owned(), + "echo \"$WITRYNA_BUILD_STATUS\" > \"$WITRYNA_BUILD_DIR/build-status.txt\"".to_owned(), + ]) + .build(); + + let server = TestServer::start(test_config_with_site(base_dir.clone(), site)).await; + + let resp = TestServer::client() + .post(server.url("/hook-success-status")) + .header("Authorization", "Bearer test-token") + .send() + .await + .unwrap(); + assert_eq!(resp.status().as_u16(), 202); + + // Wait for current symlink + let builds_dir = base_dir.join("builds/hook-success-status"); + let max_wait = Duration::from_secs(120); + let start = std::time::Instant::now(); + + loop { + assert!(start.elapsed() <= max_wait, "build timed out"); + if builds_dir.join("current").is_symlink() { + tokio::time::sleep(Duration::from_secs(3)).await; + break; + } + tokio::time::sleep(Duration::from_millis(500)).await; + } + + // Read build-status.txt from build dir + let current_target = tokio::fs::read_link(builds_dir.join("current")) + .await + .expect("current symlink should exist"); + let status_path = current_target.join("build-status.txt"); + assert!( + status_path.exists(), + "hook should have created build-status.txt" + ); + let status = tokio::fs::read_to_string(&status_path).await.unwrap(); + assert_eq!( + status.trim(), + "success", + "hook should receive build_status=success" + ); +} -- cgit v1.2.3