diff --git a/CLAUDE.md b/CLAUDE.md index fa946140f7beac0eb84bce78e18fdd18ed77d526..7b714bb89828d90a5a20b6fba3762e64ab743347 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,7 @@ This is **common-ci-configuration** - a shared CI/CD template library for Hive b Detailed documentation is available in `docs/`: - `cache-manager.md` - NFS-backed cache system for HAF/hive replay data - `common-ci-images.md` - Docker images, their purposes, and Python versions +- `haf-app-testing.md` - Templates for HAF-dependent application testing ## Validation Commands @@ -57,6 +58,8 @@ Templates are in `templates/` and are included by downstream projects: | `npm_projects.gitlab-ci.yml` | npm/pnpm package builds | | `test_jobs.gitlab-ci.yml` | pytest, jmeter, tox test runners | | `python_projects.gitlab-ci.yml` | Python linting/testing | +| `haf_app_testing.gitlab-ci.yml` | HAF app change detection, DinD testing, Tavern | +| `cache-manager.gitlab-ci.yml` | Cache-manager script setup | | `base.gitlab-ci.yml` | Common job defaults | ## Pipeline Skip Variables diff --git a/docs/haf-app-testing-templates.md b/docs/haf-app-testing-templates.md new file mode 100644 index 0000000000000000000000000000000000000000..1e7a1efdde395b3bd29cff1969c089c3cdde7390 --- /dev/null +++ b/docs/haf-app-testing-templates.md @@ -0,0 +1,107 @@ +# HAF App Testing Templates + +Reusable CI templates for HAF-dependent applications (balance_tracker, reputation_tracker, hafah, hivemind). + +## Overview + +The `templates/haf_app_testing.gitlab-ci.yml` provides composable building blocks for common CI patterns across HAF applications. Apps can use `!reference` tags to include only the pieces they need while keeping app-specific logic inline. + +## Available Templates + +### Change Detection +- `.haf_app_detect_changes` - Detects if only tests/docs changed, enabling skip of heavy sync jobs + +### Sync Job Components +- `.haf_app_sync_variables` - Common directory and compose variables +- `.haf_app_sync_setup` - Docker login and git safe.directory +- `.haf_app_fetch_haf_cache` - Simple HAF cache lookup (local then NFS) +- `.haf_app_smart_cache_lookup` - Advanced lookup with QUICK_TEST and AUTO_SKIP_SYNC support +- `.haf_app_copy_datadir` - Copy datadir and fix postgres ownership +- `.haf_app_copy_blockchain` - Copy block_log to docker directory +- `.haf_app_sync_shutdown` - PostgreSQL checkpoint, collect logs, compose down +- `.haf_app_sync_save_cache` - Save to local and push to NFS (unconditional) +- `.haf_app_sync_save_cache_conditional` - Only saves if CACHE_HIT is false +- `.haf_app_sync_cleanup` - after_script cleanup +- `.haf_app_sync_artifacts` - Standard artifacts configuration + +### Tavern Test Components +- `.tavern_test_variables` - pytest workers, tavern version, directories +- `.tavern_install_deps` - Install tavern in a venv +- `.tavern_run_tests` - Run pytest with tavern +- `.tavern_test_artifacts` - Artifacts for tavern tests + +### Skip Rules +- `.skip_on_quick_test` - Skip job when QUICK_TEST=true +- `.skip_on_auto_skip` - Skip job when AUTO_SKIP_SYNC=true +- `.skip_on_cached_data` - Skip on either condition + +## Usage Example + +```yaml +include: + - project: 'hive/common-ci-configuration' + ref: develop + file: '/templates/haf_app_testing.gitlab-ci.yml' + +sync: + extends: + - .docker_image_builder_job_template + - .haf_app_sync_variables + variables: + APP_SYNC_CACHE_TYPE: "${MY_APP_SYNC_CACHE_TYPE}" + APP_CACHE_KEY: "${MY_APP_CACHE_KEY}" + before_script: + - !reference [.haf_app_sync_setup, script] + - !reference [.haf_app_smart_cache_lookup, script] + script: + - # App-specific startup logic + - !reference [.haf_app_sync_shutdown, script] + - !reference [.haf_app_sync_save_cache_conditional, script] + after_script: !reference [.haf_app_sync_cleanup, after_script] + artifacts: !reference [.haf_app_sync_artifacts, artifacts] +``` + +## Migrated Applications + +| Application | Branch | Status | +|-------------|--------|--------| +| reputation_tracker | feature/use-common-ci-templates | Migrated | +| balance_tracker | feature/use-common-ci-templates | Migrated | +| hafah | - | Not started | +| hivemind | - | Not started | + +## Future Plans + +### Short-term +1. **Template `.test-with-docker-compose`** - The test job pattern from reputation_tracker that extracts cache, starts compose, waits for services. Could be reused across apps. + +2. **Migrate hafah** - hafah has similar patterns but runs pytest inside a docker container rather than in the CI job. May need variant templates. + +3. **Migrate hivemind** - More complex app with multiple test types, but could use sync templates. + +### Medium-term +4. **Template docker build jobs** - The `.docker-build-template` pattern is similar across apps. + +5. **Template prepare_haf_image/prepare_haf_data** - These jobs are nearly identical across apps. + +6. **Add cache cleanup templates** - Automated cleanup of old sync caches. + +### Long-term +7. **Unified HAF app CI base** - A single extensible template that apps configure rather than compose from pieces. + +8. **Cache warming jobs** - Proactive cache population for common HAF commits. + +## Variable Reference + +### Required for sync templates +- `HAF_COMMIT` - HAF submodule commit SHA +- `APP_SYNC_CACHE_TYPE` - App-specific cache type (e.g., "haf_btracker_sync") +- `APP_CACHE_KEY` - Cache key (typically `${HAF_COMMIT}_${CI_COMMIT_SHORT_SHA}`) + +### From detect_changes (optional) +- `AUTO_SKIP_SYNC` - "true" when only tests/docs changed +- `AUTO_CACHE_HAF_COMMIT` - HAF commit for cached data + +### For QUICK_TEST mode +- `QUICK_TEST` - Set to "true" to enable +- `QUICK_TEST_HAF_COMMIT` - HAF commit to use for cache diff --git a/docs/haf-app-testing.md b/docs/haf-app-testing.md new file mode 100644 index 0000000000000000000000000000000000000000..b915550df80f8a863806cc3924688aa38cc2e9e3 --- /dev/null +++ b/docs/haf-app-testing.md @@ -0,0 +1,332 @@ +# HAF Application Testing Templates + +Reusable CI templates for applications that depend on HAF (Hive Application Framework). + +## Overview + +These templates standardize common CI patterns found across HAF-dependent applications like HAfAH, reputation_tracker, balance_tracker, and hivemind. They provide: + +- **Change detection** with automatic skip logic +- **Cache management** with NFS backing +- **Docker-in-Docker test infrastructure** +- **Tavern API testing** framework + +## Quick Start + +Include the templates in your `.gitlab-ci.yml`: + +```yaml +include: + - project: 'hive/common-ci-configuration' + ref: develop + file: '/templates/haf_app_testing.gitlab-ci.yml' + +stages: + - detect + - build + - sync + - test + - cleanup + +detect_changes: + extends: .haf_app_detect_changes + variables: + HAF_APP_CACHE_TYPE: "haf_myapp_sync" + +api_tests: + extends: .haf_app_tavern_tests + variables: + HAF_APP_CACHE_TYPE: "haf_myapp_sync" + HAF_APP_CACHE_KEY: "${HAF_COMMIT}_${CI_COMMIT_SHORT_SHA}" + TEST_DIR: "tests/api_tests" +``` + +## Templates Reference + +### .fetch_cache_manager + +Sets up the cache-manager.sh script for use in jobs. + +```yaml +my_job: + extends: .fetch_cache_manager + script: + - $CACHE_MANAGER get haf $HAF_COMMIT /data +``` + +**Variables:** +| Variable | Default | Description | +|----------|---------|-------------| +| `CACHE_MANAGER` | `/tmp/cache-manager.sh` | Path where script is saved | +| `CACHE_MANAGER_REF` | `develop` | Git ref to fetch from | + +### .haf_app_detect_changes + +Analyzes changed files to determine if full HAF sync can be skipped. When only tests, docs, or CI config change, cached HAF data can be reused. + +```yaml +detect_changes: + extends: .haf_app_detect_changes + variables: + HAF_APP_CACHE_TYPE: "haf_btracker_sync" + HAF_APP_SKIP_PATTERNS: "^tests/|^docs/|\.md$" +``` + +**Required Variables:** +| Variable | Description | +|----------|-------------| +| `HAF_APP_CACHE_TYPE` | Cache type name (e.g., `haf_btracker_sync`) | + +**Optional Variables:** +| Variable | Default | Description | +|----------|---------|-------------| +| `HAF_APP_SKIP_PATTERNS` | `^tests/\|^docs/\|\.md$\|^README\|^CHANGELOG\|^LICENSE\|^CLAUDE\|^\.gitlab-ci` | Regex patterns for files that don't require sync | + +**Output (dotenv artifact):** +| Variable | Description | +|----------|-------------| +| `AUTO_SKIP_SYNC` | `"true"` if sync can be skipped | +| `AUTO_CACHE_KEY` | Cache key to use | +| `AUTO_HAF_COMMIT` | HAF commit from discovered cache | + +### .haf_commit_validation + +Ensures HAF_COMMIT variable matches include ref and submodule commit. + +```yaml +validate_haf: + extends: .haf_commit_validation + variables: + HAF_COMMIT: "1ac5d12439b9cca33e6db383adc59967cae75fc4" + HAF_INCLUDE_REF: "1ac5d12439b9cca33e6db383adc59967cae75fc4" +``` + +**Variables:** +| Variable | Default | Description | +|----------|---------|-------------| +| `HAF_COMMIT` | (required) | Expected HAF commit | +| `HAF_INCLUDE_REF` | (optional) | Should match include `ref:` | +| `HAF_SUBMODULE_PATH` | `haf` | Path to HAF submodule | + +### .haf_app_dind_test_base + +Base template for running tests with Docker Compose in a DinD environment. + +```yaml +my_tests: + extends: .haf_app_dind_test_base + variables: + HAF_APP_CACHE_TYPE: "haf_myapp_sync" + HAF_APP_CACHE_KEY: "${HAF_COMMIT}_${CI_COMMIT_SHORT_SHA}" + COMPOSE_FILE: "docker/docker-compose-test.yml" + script: + - pytest tests/ +``` + +**Required Variables:** +| Variable | Description | +|----------|-------------| +| `HAF_APP_CACHE_TYPE` | Cache type to extract | +| `HAF_APP_CACHE_KEY` | Cache key (usually from detect_changes or sync job) | + +**Optional Variables:** +| Variable | Default | Description | +|----------|---------|-------------| +| `HAF_DATA_DIRECTORY` | `${CI_PROJECT_DIR}/.haf-data` | Where to extract cache | +| `COMPOSE_FILE` | `docker/docker-compose-test.yml` | Docker Compose file path | +| `COMPOSE_PROJECT_NAME` | `haf-test-${CI_JOB_ID}` | Compose project name | +| `HAF_EXTRACT_TIMEOUT` | `300` | Timeout for cache extraction | + +**Features:** +- Automatic cache extraction via cache-manager +- Docker Compose service orchestration +- Health check waiting +- Automatic log collection and cleanup + +### .haf_app_tavern_tests + +Specialized template for Tavern-based API testing. + +```yaml +api_tests: + extends: .haf_app_tavern_tests + variables: + HAF_APP_CACHE_TYPE: "haf_myapp_sync" + HAF_APP_CACHE_KEY: "${HAF_COMMIT}_${CI_COMMIT_SHORT_SHA}" + TEST_DIR: "tests/api_tests" + PYTEST_WORKERS: "8" +``` + +**Optional Variables:** +| Variable | Default | Description | +|----------|---------|-------------| +| `PYTEST_WORKERS` | `4` | Number of parallel pytest workers | +| `TAVERN_VERSION` | `2.2.0` | Tavern package version | +| `PYTEST_ARGS` | (empty) | Additional pytest arguments | +| `TEST_DIR` | `tests/api_tests` | Directory containing tests | +| `JUNIT_REPORT` | `tavern-results.xml` | JUnit output filename | + +### .haf_app_cache_cleanup + +Manual cleanup job for local cache data. + +```yaml +cleanup_cache: + extends: .haf_app_cache_cleanup + variables: + HAF_APP_CACHE_TYPE: "haf_myapp_sync" +``` + +## Rule Templates + +### .skip_on_quick_test + +Skip job when `QUICK_TEST=true`: + +```yaml +sync: + extends: .skip_on_quick_test + script: + - # Full sync logic +``` + +### .skip_on_auto_skip + +Skip job when `AUTO_SKIP_SYNC=true` (from detect_changes): + +```yaml +sync: + extends: .skip_on_auto_skip + script: + - # Full sync logic +``` + +### .skip_on_cached_data + +Skip job when either skip condition is true: + +```yaml +sync: + extends: .skip_on_cached_data + script: + - # Full sync logic +``` + +## Quick Test Mode + +For rapid iteration, set these variables to skip full HAF sync: + +```yaml +# In pipeline variables: +QUICK_TEST: "true" +QUICK_TEST_HAF_COMMIT: "abc123..." +``` + +Find available caches: +```bash +ssh hive-builder-10 'ls -lt /nfs/ci-cache/haf_myapp_sync/*.tar | head -5' +``` + +## Cache Key Format + +Cache keys follow the pattern: `_` + +Example: `1ac5d12439b9cca33e6db383adc59967cae75fc4_a1b2c3d` + +This allows: +- Same HAF data to be shared when only app code changes +- Full rebuild when HAF commit changes +- Easy identification of cache contents + +## Migration Guide + +### From inline cache-manager fetch + +**Before:** +```yaml +before_script: + - CACHE_MANAGER=/tmp/cache-manager.sh + - curl -fsSL "https://gitlab.syncad.com/..." -o "$CACHE_MANAGER" + - chmod +x "$CACHE_MANAGER" +``` + +**After:** +```yaml +extends: .fetch_cache_manager +``` + +### From custom change detection + +**Before:** +```yaml +detect_changes: + script: + - # 80+ lines of detection logic +``` + +**After:** +```yaml +detect_changes: + extends: .haf_app_detect_changes + variables: + HAF_APP_CACHE_TYPE: "haf_myapp_sync" +``` + +### From custom DinD test jobs + +**Before:** +```yaml +tests: + image: docker-builder + services: + - docker:dind + before_script: + - # 50+ lines of setup +``` + +**After:** +```yaml +tests: + extends: .haf_app_dind_test_base + variables: + HAF_APP_CACHE_TYPE: "haf_myapp_sync" + HAF_APP_CACHE_KEY: "$CACHE_KEY" +``` + +## Best Practices + +1. **Use consistent cache type names**: Prefix with `haf_` for automatic pgdata permission handling + +2. **Pin CACHE_MANAGER_REF in production**: Use a specific commit or tag rather than `develop` + +3. **Set appropriate timeouts**: Override `HAF_SYNC_TIMEOUT` and `HAF_TEST_TIMEOUT` for your workload + +4. **Use proper runner tags**: Jobs using cache need `data-cache-storage` and optionally `fast` + +5. **Validate HAF commit**: Use `.haf_commit_validation` to catch version mismatches early + +## Troubleshooting + +### Cache miss when expected hit + +1. Check cache type matches exactly (case-sensitive) +2. Verify cache key format matches expected pattern +3. Check NFS availability: `mountpoint /nfs/ci-cache` + +### Services not starting + +1. Check compose file path is correct +2. Verify image names and tags +3. Check container logs in artifacts + +### Slow cache extraction + +This is expected for large HAF caches (~19GB). Use `fast` runner tag for better performance. + +### Permission errors with pgdata + +Ensure cache type starts with `haf_` for automatic permission handling, or manually fix: +```bash +sudo chown -R 105:105 /path/to/pgdata +sudo chmod 700 /path/to/pgdata +``` diff --git a/templates/haf_app_testing.gitlab-ci.yml b/templates/haf_app_testing.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..1e1df2c7a18e559102d4e89042f4ea11874fd876 --- /dev/null +++ b/templates/haf_app_testing.gitlab-ci.yml @@ -0,0 +1,848 @@ +# HAF Application Testing Templates +# +# Provides reusable CI patterns for HAF-dependent applications. +# These templates standardize common patterns found across HAfAH, +# reputation_tracker, balance_tracker, and hivemind. +# +# Usage in your .gitlab-ci.yml: +# include: +# - project: 'hive/common-ci-configuration' +# ref: develop +# file: '/templates/haf_app_testing.gitlab-ci.yml' +# +# detect_changes: +# extends: .haf_app_detect_changes +# variables: +# HAF_APP_CACHE_TYPE: "haf_myapp_sync" +# +# my_test_job: +# extends: .haf_app_dind_test_base +# script: +# - pytest tests/ + +variables: + # Docker image versions - use specific commit with util-linux for NFS flock support + # Override these if you need a different version + DOCKER_BUILDER_TAG: "3dd346380da7f28cdaf3ef3955559c905d94bd1c" + DOCKER_DIND_TAG: "3dd346380da7f28cdaf3ef3955559c905d94bd1c" + + # Cache manager configuration + CACHE_MANAGER: "/tmp/cache-manager.sh" + CACHE_MANAGER_REF: "develop" + + # NFS and local cache paths (standard across all builders) + DATA_CACHE_NFS_PREFIX: "/nfs/ci-cache" + DATA_CACHE_HAF_PREFIX: "/cache/replay_data_haf" + DATA_CACHE_LOCAL_PREFIX: "/cache" + + # Common skip patterns for change detection + # Files matching these patterns don't require full HAF sync + HAF_APP_SKIP_PATTERNS: '^tests/|^docs/|\.md$|^README|^CHANGELOG|^LICENSE|^CLAUDE|^\.gitlab-ci' + + # Default timeouts (can be overridden per job) + HAF_SYNC_TIMEOUT: "7200" # 2 hours for sync jobs + HAF_TEST_TIMEOUT: "1800" # 30 minutes for test jobs + HAF_EXTRACT_TIMEOUT: "300" # 5 minutes for cache extraction + +include: + - local: templates/base.gitlab-ci.yml + - local: templates/docker_image_jobs.gitlab-ci.yml + +# ============================================================================= +# CACHE MANAGER SETUP +# ============================================================================= +# Standardized cache-manager.sh fetching that all HAF apps should use. +# This ensures consistent caching behavior across all applications. + +.fetch_cache_manager: + before_script: + - | + echo -e "\e[0Ksection_start:$(date +%s):cache_manager_setup[collapsed=true]\r\e[0KSetting up cache-manager..." + mkdir -p "$(dirname "$CACHE_MANAGER")" + curl -fsSL "https://gitlab.syncad.com/hive/common-ci-configuration/-/raw/${CACHE_MANAGER_REF}/scripts/cache-manager.sh" -o "$CACHE_MANAGER" + chmod +x "$CACHE_MANAGER" + echo "Cache manager ready: $CACHE_MANAGER (ref: ${CACHE_MANAGER_REF})" + echo -e "\e[0Ksection_end:$(date +%s):cache_manager_setup\r\e[0K" + +# ============================================================================= +# CHANGE DETECTION TEMPLATE +# ============================================================================= +# Simplified detect_changes template - analyzes changed files to determine +# if full HAF sync/build can be skipped. When only tests/docs change, we can +# use cached HAF data. +# +# Required variables to set in derived job: +# HAF_APP_SKIP_PATTERNS: Regex patterns for files that don't need sync +# HAF_COMMIT: HAF commit (for cache key lookup) +# +# Outputs (via dotenv artifact): +# AUTO_SKIP_BUILD: "true" if build/sync can be skipped +# AUTO_SKIP_SYNC: alias for AUTO_SKIP_BUILD (compatibility) +# CACHE_COMMIT: HAF commit to use for cached data +# AUTO_CACHE_HAF_COMMIT: alias for CACHE_COMMIT (compatibility) +# AUTO_CACHE_COMMIT: alias for CACHE_COMMIT (compatibility) +# +# Note: This template handles the common case. For apps with custom cache +# discovery logic, you can override the script section. + +.haf_app_detect_changes: + extends: .job-defaults + stage: detect + image: alpine:latest + variables: + GIT_SUBMODULE_STRATEGY: none + before_script: + - apk add --no-cache git + script: + - | + set -e + + # Initialize defaults + CAN_SKIP_BUILD=false + CACHE_COMMIT="" + + # Handle manual QUICK_TEST mode + if [ "$QUICK_TEST" = "true" ]; then + echo "=== Manual QUICK_TEST Mode ===" + CAN_SKIP_BUILD=true + CACHE_COMMIT="${QUICK_TEST_HAF_COMMIT}" + if [ -z "$CACHE_COMMIT" ]; then + echo "ERROR: QUICK_TEST_HAF_COMMIT must be set when QUICK_TEST=true" + echo "" + echo "Find available caches:" + echo " ssh hive-builder-10 'ls -lt /nfs/ci-cache/haf/*.tar | head -5'" + exit 1 + fi + echo "Using cached data from: $CACHE_COMMIT" + else + # Automatic detection mode + echo "=== Detecting Changed Files ===" + + # Get changed files based on pipeline type + if [ -n "${CI_MERGE_REQUEST_DIFF_BASE_SHA:-}" ]; then + BASE_SHA="$CI_MERGE_REQUEST_DIFF_BASE_SHA" + echo "MR pipeline: comparing against target branch" + elif [ "${CI_PIPELINE_SOURCE:-}" = "push" ]; then + BASE_SHA="HEAD~1" + echo "Push pipeline: comparing against previous commit" + else + BASE_SHA=$(git merge-base HEAD origin/develop 2>/dev/null || echo "HEAD~1") + echo "Other pipeline: comparing against develop" + fi + + echo "Comparing $BASE_SHA to HEAD" + CHANGED_FILES=$(git diff --name-only "$BASE_SHA" HEAD 2>/dev/null || git show --name-only --pretty=format: HEAD | grep -v '^$' || true) + + # Last resort: require full build + if [ -z "$CHANGED_FILES" ]; then + echo "WARNING: Could not determine changed files - requiring full build" + CHANGED_FILES="unknown-changes" + fi + + echo "Changed files:" + echo "$CHANGED_FILES" | head -50 + + NEEDS_BUILD=$(echo "$CHANGED_FILES" | grep -vE "${HAF_APP_SKIP_PATTERNS}" || true) + + if [ -z "$NEEDS_BUILD" ]; then + echo "" + echo "=== Can skip data prep (only tests/docs changed) ===" + CAN_SKIP_BUILD=true + # Use current HAF submodule commit as cache key + CACHE_COMMIT="${HAF_COMMIT}" + else + echo "" + echo "=== Files requiring full build: ===" + echo "$NEEDS_BUILD" + fi + fi + + # Write dotenv artifact (output multiple names for compatibility across apps) + echo "AUTO_SKIP_BUILD=${CAN_SKIP_BUILD}" > detect_changes.env + echo "AUTO_SKIP_SYNC=${CAN_SKIP_BUILD}" >> detect_changes.env + echo "CACHE_COMMIT=${CACHE_COMMIT}" >> detect_changes.env + echo "AUTO_CACHE_HAF_COMMIT=${CACHE_COMMIT}" >> detect_changes.env + echo "AUTO_CACHE_COMMIT=${CACHE_COMMIT}" >> detect_changes.env + + echo "" + echo "=== Detection Results ===" + echo "AUTO_SKIP_BUILD/SYNC: $CAN_SKIP_BUILD" + echo "CACHE_COMMIT: $CACHE_COMMIT" + + artifacts: + reports: + dotenv: detect_changes.env + expire_in: 1 day + rules: + # Never run for tags (always full build for releases) + - if: $CI_COMMIT_TAG + when: never + - when: on_success + +# ============================================================================= +# HAF COMMIT VALIDATION TEMPLATE +# ============================================================================= +# Ensures HAF_COMMIT variable matches include ref and submodule commit. +# This prevents subtle bugs from version mismatches. + +.haf_commit_validation: + extends: .job-defaults + stage: build + image: alpine:latest + variables: + HAF_COMMIT: "" # Must be set by derived job + HAF_INCLUDE_REF: "" # Should match the 'ref' in your HAF include + HAF_SUBMODULE_PATH: "haf" # Path to HAF submodule + before_script: + - apk add --no-cache git + script: + - | + set -euo pipefail + + echo "=== HAF Commit Validation ===" + echo "HAF_COMMIT variable: ${HAF_COMMIT}" + echo "HAF_INCLUDE_REF: ${HAF_INCLUDE_REF:-not set}" + echo "HAF_SUBMODULE_PATH: ${HAF_SUBMODULE_PATH}" + + ERRORS=0 + + # Check submodule commit matches + if [[ -d "${HAF_SUBMODULE_PATH}/.git" ]] || [[ -f "${HAF_SUBMODULE_PATH}/.git" ]]; then + SUBMODULE_COMMIT=$(git -C "${HAF_SUBMODULE_PATH}" rev-parse HEAD 2>/dev/null || echo "") + echo "Submodule commit: ${SUBMODULE_COMMIT}" + + if [[ -n "$SUBMODULE_COMMIT" && "$SUBMODULE_COMMIT" != "$HAF_COMMIT" ]]; then + echo "ERROR: HAF_COMMIT ($HAF_COMMIT) does not match submodule ($SUBMODULE_COMMIT)" + ERRORS=$((ERRORS + 1)) + fi + else + echo "WARNING: HAF submodule not found at ${HAF_SUBMODULE_PATH}" + fi + + # Check include ref matches (if provided) + if [[ -n "${HAF_INCLUDE_REF:-}" && "$HAF_INCLUDE_REF" != "$HAF_COMMIT" ]]; then + echo "ERROR: HAF_COMMIT ($HAF_COMMIT) does not match include ref ($HAF_INCLUDE_REF)" + ERRORS=$((ERRORS + 1)) + fi + + if [[ $ERRORS -gt 0 ]]; then + echo "" + echo "VALIDATION FAILED: $ERRORS errors found" + echo "Please ensure HAF_COMMIT, include ref, and submodule are all in sync" + exit 1 + fi + + echo "" + echo "VALIDATION PASSED" + tags: + - public-runner-docker + +# ============================================================================= +# DOCKER-IN-DOCKER TEST BASE TEMPLATE +# ============================================================================= +# Base template for running tests with Docker Compose in a DinD environment. +# Provides proper cache extraction and service orchestration. +# +# Required variables: +# HAF_APP_CACHE_TYPE: Cache type to extract +# HAF_APP_CACHE_KEY: Cache key (usually from detect_changes or sync job) +# +# Optional: +# COMPOSE_FILE: Path to docker-compose file (default: docker/docker-compose-test.yml) +# COMPOSE_PROJECT_NAME: Docker Compose project name +# HAF_DATA_DIRECTORY: Where to extract cache data + +.haf_app_dind_test_base: + extends: .docker_image_builder_job_template + stage: test + timeout: 30 minutes + variables: + FF_NETWORK_PER_BUILD: "true" + DOCKER_TLS_CERTDIR: "" + DOCKER_HOST: "tcp://docker:2375" + + # Cache configuration + HAF_APP_CACHE_TYPE: "" # Must be set + HAF_APP_CACHE_KEY: "" # Must be set (from detect_changes or sync) + HAF_DATA_DIRECTORY: "${CI_PROJECT_DIR}/.haf-data" + + # Docker Compose configuration + COMPOSE_FILE: "docker/docker-compose-test.yml" + COMPOSE_PROJECT_NAME: "haf-test-${CI_JOB_ID}" + + before_script: + - !reference [.docker_image_builder_job_template, before_script] + - | + echo -e "\e[0Ksection_start:$(date +%s):cache_setup[collapsed=true]\r\e[0KSetting up HAF cache..." + + # Fetch cache-manager + mkdir -p "$(dirname "$CACHE_MANAGER")" + curl -fsSL "https://gitlab.syncad.com/hive/common-ci-configuration/-/raw/${CACHE_MANAGER_REF}/scripts/cache-manager.sh" -o "$CACHE_MANAGER" + chmod +x "$CACHE_MANAGER" + + # Extract cache + echo "Extracting cache: ${HAF_APP_CACHE_TYPE}/${HAF_APP_CACHE_KEY}" + mkdir -p "${HAF_DATA_DIRECTORY}" + "$CACHE_MANAGER" get "${HAF_APP_CACHE_TYPE}" "${HAF_APP_CACHE_KEY}" "${HAF_DATA_DIRECTORY}" + + echo -e "\e[0Ksection_end:$(date +%s):cache_setup\r\e[0K" + - | + echo -e "\e[0Ksection_start:$(date +%s):compose_start[collapsed=true]\r\e[0KStarting Docker Compose services..." + + # Start services + if [[ -f "${COMPOSE_FILE}" ]]; then + docker compose -f "${COMPOSE_FILE}" -p "${COMPOSE_PROJECT_NAME}" up -d --quiet-pull + + # Wait for services to be healthy + echo "Waiting for services to be ready..." + WAIT_START=$(date +%s) + WAIT_TIMEOUT=${HAF_EXTRACT_TIMEOUT:-300} + + while true; do + UNHEALTHY=$(docker compose -f "${COMPOSE_FILE}" -p "${COMPOSE_PROJECT_NAME}" ps --format json 2>/dev/null | jq -r 'select(.Health != "healthy" and .Health != "" and .State == "running") | .Name' | wc -l || echo "0") + + if [[ "$UNHEALTHY" == "0" ]]; then + echo "All services healthy" + break + fi + + ELAPSED=$(($(date +%s) - WAIT_START)) + if [[ $ELAPSED -ge $WAIT_TIMEOUT ]]; then + echo "WARNING: Timeout waiting for services (${WAIT_TIMEOUT}s)" + docker compose -f "${COMPOSE_FILE}" -p "${COMPOSE_PROJECT_NAME}" ps + break + fi + + echo "Waiting for $UNHEALTHY services... (${ELAPSED}s)" + sleep 5 + done + else + echo "No compose file found at ${COMPOSE_FILE}, skipping service startup" + fi + + echo -e "\e[0Ksection_end:$(date +%s):compose_start\r\e[0K" + + after_script: + - | + echo -e "\e[0Ksection_start:$(date +%s):compose_cleanup\r\e[0KCleaning up Docker Compose..." + + if [[ -f "${COMPOSE_FILE:-docker/docker-compose-test.yml}" ]]; then + # Capture logs before cleanup + docker compose -f "${COMPOSE_FILE:-docker/docker-compose-test.yml}" -p "${COMPOSE_PROJECT_NAME:-haf-test}" logs --no-color > container-logs.txt 2>&1 || true + + # Cleanup + docker compose -f "${COMPOSE_FILE:-docker/docker-compose-test.yml}" -p "${COMPOSE_PROJECT_NAME:-haf-test}" down -v --remove-orphans || true + fi + + echo -e "\e[0Ksection_end:$(date +%s):compose_cleanup\r\e[0K" + + artifacts: + when: always + paths: + - container-logs.txt + expire_in: 1 week + + tags: + - data-cache-storage + - fast + +# ============================================================================= +# TAVERN TEST HELPERS +# ============================================================================= +# Composable templates for running Tavern API tests. +# Apps can mix these with their own DinD/cache setup. + +# Variables for tavern tests (set these in your job) +.tavern_test_variables: + variables: + PYTEST_WORKERS: "4" + TAVERN_VERSION: "2.2.0" + TAVERN_DIR: "tests/tavern" + JUNIT_REPORT: "tests/tavern/report.xml" + +# Script to install tavern dependencies in a venv +# Use with: !reference [.tavern_install_deps, script] +.tavern_install_deps: + script: + - | + echo -e "\e[0Ksection_start:$(date +%s):tavern_deps[collapsed=true]\r\e[0KInstalling Tavern dependencies..." + python3 -m venv venv + . venv/bin/activate + pip install --quiet \ + "tavern==${TAVERN_VERSION:-2.2.0}" \ + "pytest-xdist>=3.2.0" \ + "pyyaml>=6.0" \ + "requests>=2.28.0" \ + "deepdiff>=6.0" \ + "prettytable>=3.0" + echo -e "\e[0Ksection_end:$(date +%s):tavern_deps\r\e[0K" + +# Script to run tavern tests with pytest +# Requires: venv activated, TAVERN_DIR set, JUNIT_REPORT set +# Use with: !reference [.tavern_run_tests, script] +.tavern_run_tests: + script: + - | + echo "=== Running Tavern API Tests ===" + . venv/bin/activate + cd "${TAVERN_DIR}" + pytest -n "${PYTEST_WORKERS:-4}" --junitxml "${JUNIT_REPORT}" . || exit $? + +# Artifacts configuration for tavern tests +# Use with: artifacts: !reference [.tavern_test_artifacts, artifacts] +.tavern_test_artifacts: + artifacts: + when: always + paths: + - "**/*.out.json" + - docker/container-logs.tar.gz + - docker/docker-compose-logs.txt + reports: + junit: "${JUNIT_REPORT}" + expire_in: 1 week + +# Combined tavern test job template (for simple cases) +# Extends .docker_image_builder_job_template, provides test execution +# Apps must provide their own before_script for cache/compose setup +.haf_app_tavern_tests: + extends: + - .docker_image_builder_job_template + - .tavern_test_variables + stage: test + timeout: 30 minutes + script: + - !reference [.tavern_install_deps, script] + - !reference [.tavern_run_tests, script] + artifacts: !reference [.tavern_test_artifacts, artifacts] + +# ============================================================================= +# QUICK TEST MODE HELPERS +# ============================================================================= +# Templates for handling QUICK_TEST mode (manual cache selection) + +.quick_test_variables: + variables: + # Set these to use quick test mode + QUICK_TEST: "" # Set to "true" to enable + QUICK_TEST_HAF_COMMIT: "" # Required if QUICK_TEST=true + QUICK_TEST_APP_COMMIT: "" # Optional, defaults to current commit + +# Rule to skip jobs when QUICK_TEST is enabled +.skip_on_quick_test: + rules: + - if: $QUICK_TEST == "true" + when: never + - when: on_success + +# Rule to skip jobs when AUTO_SKIP_SYNC is true (from detect_changes) +.skip_on_auto_skip: + rules: + - if: $AUTO_SKIP_SYNC == "true" + when: never + - when: on_success + +# Combined rule for both skip conditions +.skip_on_cached_data: + rules: + - if: $QUICK_TEST == "true" + when: never + - if: $AUTO_SKIP_SYNC == "true" + when: never + - when: on_success + +# ============================================================================= +# SYNC JOB COMPONENTS +# ============================================================================= +# Composable building blocks for HAF app sync jobs. +# Use !reference to compose your sync job from these components. +# +# Typical sync job structure: +# extends: .docker_image_builder_job_template +# before_script: +# - !reference [.haf_app_sync_setup, script] +# - !reference [.haf_app_fetch_haf_cache, script] +# script: +# - # Your app-specific sync logic +# - !reference [.haf_app_sync_shutdown, script] +# - !reference [.haf_app_sync_save_cache, script] +# after_script: !reference [.haf_app_sync_cleanup, after_script] +# artifacts: !reference [.haf_app_sync_artifacts, artifacts] + +# Common sync job variables +.haf_app_sync_variables: + variables: + # Directories (using CI_JOB_ID for isolation) + DATADIR: "${CI_PROJECT_DIR}/${CI_JOB_ID}/datadir" + SHM_DIR: "${CI_PROJECT_DIR}/${CI_JOB_ID}/shm_dir" + HAF_DATA_DIRECTORY: "${DATADIR}" + HAF_SHM_DIRECTORY: "${SHM_DIR}" + # Docker compose options (override per-app) + COMPOSE_OPTIONS_STRING: "--env-file ci.env --file docker-compose.yml --ansi never" + # Timeout for graceful PostgreSQL shutdown + COMPOSE_DOWN_TIMEOUT: "60" + # Postgres user in HAF container (UID:GID) + POSTGRES_UID: "105" + POSTGRES_GID: "109" + +# Docker login and git safe.directory setup +# Use: !reference [.haf_app_sync_setup, script] +.haf_app_sync_setup: + script: + - | + echo -e "\e[0Ksection_start:$(date +%s):login[collapsed=true]\r\e[0KLogging to Docker registry..." + docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + echo -e "\e[0Ksection_end:$(date +%s):login\r\e[0K" + echo -e "\e[0Ksection_start:$(date +%s):git[collapsed=true]\r\e[0KConfiguring Git..." + git config --global --add safe.directory "$CI_PROJECT_DIR" + git config --global --add safe.directory "$CI_PROJECT_DIR/haf" + echo -e "\e[0Ksection_end:$(date +%s):git\r\e[0K" + +# Fetch HAF replay data from local cache or NFS (simple version) +# Requires: HAF_COMMIT, DATA_CACHE_HAF_PREFIX variables +# Use: !reference [.haf_app_fetch_haf_cache, script] +.haf_app_fetch_haf_cache: + script: + - | + echo -e "\e[0Ksection_start:$(date +%s):haf_cache[collapsed=true]\r\e[0KFetching HAF replay data..." + LOCAL_HAF_CACHE="${DATA_CACHE_HAF_PREFIX}_${HAF_COMMIT}" + if [[ -d "${LOCAL_HAF_CACHE}/datadir" ]]; then + echo "Local HAF cache found at ${LOCAL_HAF_CACHE}" + else + echo "Local HAF cache not found, fetching from NFS..." + # Fetch cache-manager if not already present + if [[ ! -x "$CACHE_MANAGER" ]]; then + curl -fsSL "https://gitlab.syncad.com/hive/common-ci-configuration/-/raw/${CACHE_MANAGER_REF:-develop}/scripts/cache-manager.sh" -o "$CACHE_MANAGER" + chmod +x "$CACHE_MANAGER" + fi + if "$CACHE_MANAGER" get haf "${HAF_COMMIT}" "${LOCAL_HAF_CACHE}"; then + echo "Fetched HAF replay data from NFS cache" + else + echo "ERROR: Failed to fetch HAF replay data from NFS cache" + exit 1 + fi + fi + echo -e "\e[0Ksection_end:$(date +%s):haf_cache\r\e[0K" + +# Advanced cache lookup with QUICK_TEST and AUTO_SKIP_SYNC support +# Checks for existing app sync cache first (avoiding re-sync), then falls back to HAF cache. +# When AUTO_SKIP_SYNC is true, also searches for any compatible app cache with matching HAF commit. +# +# Requires variables: +# HAF_COMMIT: Current HAF submodule commit +# APP_SYNC_CACHE_TYPE: App-specific cache type (e.g., "haf_btracker_sync") +# DATA_CACHE_HAF_PREFIX: Local cache directory prefix +# DATA_CACHE_NFS_PREFIX: NFS cache directory prefix +# +# Optional variables (from detect_changes job): +# QUICK_TEST: Set to "true" to use QUICK_TEST_HAF_COMMIT +# QUICK_TEST_HAF_COMMIT: HAF commit to use in quick test mode +# AUTO_SKIP_SYNC: Set to "true" when only tests/docs changed +# AUTO_CACHE_HAF_COMMIT: HAF commit from detect_changes +# +# Outputs (written to /tmp for use in script section): +# /tmp/cache_hit: "true" if app sync cache was found (skip sync) +# /tmp/effective_cache_key: The cache key to use for saving +# /tmp/effective_haf_commit: The HAF commit being used +# +# Use: !reference [.haf_app_smart_cache_lookup, script] +.haf_app_smart_cache_lookup: + script: + - | + echo -e "\e[0Ksection_start:$(date +%s):cache_lookup[collapsed=true]\r\e[0KSmart cache lookup..." + + # Determine effective HAF commit based on mode + EFFECTIVE_HAF_COMMIT="${HAF_COMMIT}" + if [[ "${QUICK_TEST:-false}" == "true" ]]; then + echo "=== QUICK_TEST Mode (manual) ===" + if [[ -z "${QUICK_TEST_HAF_COMMIT}" ]]; then + echo "ERROR: QUICK_TEST=true but QUICK_TEST_HAF_COMMIT not set" + echo "" + echo "Find available caches:" + echo " ssh hive-builder-10 'ls -lt /nfs/ci-cache/haf/*.tar | head -5'" + exit 1 + fi + EFFECTIVE_HAF_COMMIT="${QUICK_TEST_HAF_COMMIT}" + echo "Using cached HAF data from: ${EFFECTIVE_HAF_COMMIT}" + elif [[ "${AUTO_SKIP_SYNC:-false}" == "true" ]] && [[ -n "${AUTO_CACHE_HAF_COMMIT}" ]]; then + echo "=== AUTO_SKIP_SYNC Mode (only tests/docs changed) ===" + EFFECTIVE_HAF_COMMIT="${AUTO_CACHE_HAF_COMMIT}" + echo "Using cached HAF data from: ${EFFECTIVE_HAF_COMMIT}" + fi + + # Build cache paths + EFFECTIVE_CACHE_KEY="${EFFECTIVE_HAF_COMMIT}_${CI_COMMIT_SHORT_SHA}" + LOCAL_APP_CACHE="${DATA_CACHE_HAF_PREFIX}_${APP_SYNC_CACHE_TYPE}_${EFFECTIVE_CACHE_KEY}" + LOCAL_HAF_CACHE="${DATA_CACHE_HAF_PREFIX}_${EFFECTIVE_HAF_COMMIT}" + + # Fetch cache-manager + if [[ ! -x "$CACHE_MANAGER" ]]; then + curl -fsSL "https://gitlab.syncad.com/hive/common-ci-configuration/-/raw/${CACHE_MANAGER_REF:-develop}/scripts/cache-manager.sh" -o "$CACHE_MANAGER" + chmod +x "$CACHE_MANAGER" + fi + + CACHE_HIT="false" + + # Check for existing app sync cache (local first, then NFS) + if [[ -d "${LOCAL_APP_CACHE}/datadir" ]]; then + echo "Local app sync cache found at ${LOCAL_APP_CACHE} - skipping sync" + CACHE_HIT="true" + export DATA_SOURCE="${LOCAL_APP_CACHE}" + else + echo "Checking NFS for app sync cache: ${APP_SYNC_CACHE_TYPE}/${EFFECTIVE_CACHE_KEY}" + if CACHE_HANDLING=haf "$CACHE_MANAGER" get "${APP_SYNC_CACHE_TYPE}" "${EFFECTIVE_CACHE_KEY}" "${LOCAL_APP_CACHE}" 2>/dev/null; then + echo "Fetched app sync cache from NFS - skipping sync" + CACHE_HIT="true" + export DATA_SOURCE="${LOCAL_APP_CACHE}" + elif [[ "${AUTO_SKIP_SYNC:-false}" == "true" ]]; then + # For docs-only changes, search for ANY app cache with matching HAF commit + # since app code hasn't changed + echo "Exact cache not found, searching for compatible app cache with HAF commit ${EFFECTIVE_HAF_COMMIT}..." + NFS_CACHE_DIR="${DATA_CACHE_NFS_PREFIX}/${APP_SYNC_CACHE_TYPE}" + FOUND_CACHE=$(ls -t "${NFS_CACHE_DIR}/${EFFECTIVE_HAF_COMMIT}_"*.tar 2>/dev/null | head -1 || true) + if [[ -n "$FOUND_CACHE" ]]; then + FOUND_KEY=$(basename "$FOUND_CACHE" .tar) + echo "Found compatible app cache: ${FOUND_KEY}" + LOCAL_APP_CACHE="${DATA_CACHE_HAF_PREFIX}_${APP_SYNC_CACHE_TYPE}_${FOUND_KEY}" + if CACHE_HANDLING=haf "$CACHE_MANAGER" get "${APP_SYNC_CACHE_TYPE}" "${FOUND_KEY}" "${LOCAL_APP_CACHE}" 2>/dev/null; then + echo "Fetched compatible app sync cache from NFS - skipping sync" + CACHE_HIT="true" + export DATA_SOURCE="${LOCAL_APP_CACHE}" + fi + fi + if [[ "$CACHE_HIT" != "true" ]]; then + echo "No compatible app cache found, will use HAF-only cache and run sync" + fi + else + echo "App sync cache not found, will use HAF-only cache and run sync" + fi + fi + + # Fall back to HAF-only cache if no app cache found + if [[ "$CACHE_HIT" != "true" ]]; then + if [[ -d "${LOCAL_HAF_CACHE}/datadir" ]]; then + echo "Local HAF cache found at ${LOCAL_HAF_CACHE}" + else + echo "Local HAF cache not found, checking NFS..." + if "$CACHE_MANAGER" get haf "${EFFECTIVE_HAF_COMMIT}" "${LOCAL_HAF_CACHE}"; then + echo "Fetched HAF replay data from NFS cache" + else + echo "ERROR: Failed to fetch HAF replay data from NFS cache" + exit 1 + fi + fi + fi + + # Export state for script section + echo "$CACHE_HIT" > /tmp/cache_hit + echo "$EFFECTIVE_CACHE_KEY" > /tmp/effective_cache_key + echo "$EFFECTIVE_HAF_COMMIT" > /tmp/effective_haf_commit + + echo "Cache lookup result: CACHE_HIT=$CACHE_HIT, KEY=$EFFECTIVE_CACHE_KEY" + echo -e "\e[0Ksection_end:$(date +%s):cache_lookup\r\e[0K" + +# Conditional save cache - only saves if CACHE_HIT is false +# Use this instead of .haf_app_sync_save_cache when using smart cache lookup +# Requires: APP_SYNC_CACHE_TYPE, DATADIR, SHM_DIR, /tmp/cache_hit, /tmp/effective_cache_key +# Use: !reference [.haf_app_sync_save_cache_conditional, script] +.haf_app_sync_save_cache_conditional: + script: + - | + CACHE_HIT=$(cat /tmp/cache_hit 2>/dev/null || echo "false") + EFFECTIVE_CACHE_KEY=$(cat /tmp/effective_cache_key 2>/dev/null || echo "${APP_CACHE_KEY}") + + if [[ "$CACHE_HIT" == "true" ]]; then + echo "Cache hit - skipping cache save (data already in cache)" + else + echo -e "\e[0Ksection_start:$(date +%s):save_cache[collapsed=true]\r\e[0KSaving sync data to cache..." + + # Prepare data for caching + sudo cp -a "${SHM_DIR}" "${DATADIR}/shm_dir" + + # Save to local cache + LOCAL_APP_CACHE="${DATA_CACHE_HAF_PREFIX}_${APP_SYNC_CACHE_TYPE}_${EFFECTIVE_CACHE_KEY}" + mkdir -p "${LOCAL_APP_CACHE}" + sudo cp -a "${DATADIR}" "${LOCAL_APP_CACHE}" + + # Remove empty blockchain dir to trigger symlink on test runners + if [[ -d "${LOCAL_APP_CACHE}/datadir/blockchain" ]] && [[ -z "$(ls -A "${LOCAL_APP_CACHE}/datadir/blockchain" 2>/dev/null)" ]]; then + echo "Removing empty blockchain directory from local cache" + rmdir "${LOCAL_APP_CACHE}/datadir/blockchain" 2>/dev/null || true + fi + + ls -lah "${LOCAL_APP_CACHE}" + ls -lah "${LOCAL_APP_CACHE}/datadir" || true + + # Push to NFS cache + if [[ ! -x "$CACHE_MANAGER" ]]; then + curl -fsSL "https://gitlab.syncad.com/hive/common-ci-configuration/-/raw/${CACHE_MANAGER_REF:-develop}/scripts/cache-manager.sh" -o "$CACHE_MANAGER" + chmod +x "$CACHE_MANAGER" + fi + + echo "Pushing sync data to NFS: ${APP_SYNC_CACHE_TYPE}/${EFFECTIVE_CACHE_KEY}" + if CACHE_HANDLING=haf "$CACHE_MANAGER" put "${APP_SYNC_CACHE_TYPE}" "${EFFECTIVE_CACHE_KEY}" "${LOCAL_APP_CACHE}"; then + echo "Successfully pushed cache to NFS" + else + echo "WARNING: Failed to push to NFS cache" + fi + echo -e "\e[0Ksection_end:$(date +%s):save_cache\r\e[0K" + fi + +# Copy datadir from HAF cache and fix ownership/permissions +# Requires: HAF submodule with scripts/copy_datadir.sh, DATADIR, POSTGRES_UID/GID +# Use: !reference [.haf_app_copy_datadir, script] +.haf_app_copy_datadir: + script: + - | + echo -e "\e[0Ksection_start:$(date +%s):copy_datadir[collapsed=true]\r\e[0KCopying HAF datadir..." + "${CI_PROJECT_DIR}/haf/scripts/copy_datadir.sh" + + # Fix pgdata ownership and permissions for HAF container + if [[ -d "${DATADIR}/haf_db_store" ]]; then + echo "Fixing haf_db_store ownership to UID ${POSTGRES_UID}:${POSTGRES_GID}" + sudo chown -R "${POSTGRES_UID}:${POSTGRES_GID}" "${DATADIR}/haf_db_store" + sudo chown -R "${POSTGRES_UID}:${POSTGRES_GID}" "${DATADIR}/haf_postgresql_conf.d" 2>/dev/null || true + # Fix pgdata permissions - PostgreSQL requires 700 or 750 + if [[ -d "${DATADIR}/haf_db_store/pgdata" ]]; then + echo "Fixing pgdata permissions to 700" + sudo chmod 700 "${DATADIR}/haf_db_store/pgdata" + fi + fi + echo -e "\e[0Ksection_end:$(date +%s):copy_datadir\r\e[0K" + +# Copy block_log to docker directory +# Requires: BLOCK_LOG_SOURCE_DIR_5M variable +# Use: !reference [.haf_app_copy_blockchain, script] +.haf_app_copy_blockchain: + script: + - | + echo -e "\e[0Ksection_start:$(date +%s):blockchain[collapsed=true]\r\e[0KCopying blockchain files..." + mkdir -p "${CI_PROJECT_DIR}/docker/blockchain" + cp "${BLOCK_LOG_SOURCE_DIR_5M}/block_log" "${CI_PROJECT_DIR}/docker/blockchain/block_log" + cp "${BLOCK_LOG_SOURCE_DIR_5M}/block_log.artifacts" "${CI_PROJECT_DIR}/docker/blockchain/block_log.artifacts" + chmod a+w docker/blockchain/block_log + echo -e "\e[0Ksection_end:$(date +%s):blockchain\r\e[0K" + +# Shutdown containers: checkpoint PostgreSQL, collect logs, compose down +# Requires: COMPOSE_OPTIONS_STRING, COMPOSE_DOWN_TIMEOUT variables +# Use: !reference [.haf_app_sync_shutdown, script] +.haf_app_sync_shutdown: + script: + - | + echo -e "\e[0Ksection_start:$(date +%s):shutdown[collapsed=true]\r\e[0KStopping containers..." + pushd docker + IFS=" " read -ra COMPOSE_OPTIONS <<< $COMPOSE_OPTIONS_STRING + + # Force PostgreSQL checkpoint before shutdown to ensure all data is written to disk + echo "Forcing PostgreSQL checkpoint..." + docker compose "${COMPOSE_OPTIONS[@]}" exec -T haf psql -U haf_admin -d haf_block_log -c "CHECKPOINT;" || true + + # Collect logs before stopping + docker compose "${COMPOSE_OPTIONS[@]}" logs haf > haf.log || true + docker compose "${COMPOSE_OPTIONS[@]}" logs backend-setup > backend-setup.log || true + docker compose "${COMPOSE_OPTIONS[@]}" logs backend-block-processing > backend-block-processing.log || true + docker compose "${COMPOSE_OPTIONS[@]}" logs backend-postgrest > backend-postgrest.log || true + + # Use longer timeout for graceful PostgreSQL shutdown + docker compose "${COMPOSE_OPTIONS[@]}" down --volumes --timeout "${COMPOSE_DOWN_TIMEOUT:-60}" + popd + + # Archive logs + tar -czvf docker/container-logs.tar.gz $(pwd)/docker/*.log + echo -e "\e[0Ksection_end:$(date +%s):shutdown\r\e[0K" + +# Save sync data to local cache and push to NFS +# Requires: APP_SYNC_CACHE_TYPE, APP_CACHE_KEY, DATADIR, SHM_DIR +# Use: !reference [.haf_app_sync_save_cache, script] +.haf_app_sync_save_cache: + script: + - | + echo -e "\e[0Ksection_start:$(date +%s):save_cache[collapsed=true]\r\e[0KSaving sync data to cache..." + + # Prepare data for caching + sudo cp -a "${SHM_DIR}" "${DATADIR}/shm_dir" + mkdir -p "${DATADIR}/blockchain" + sudo cp -a "${CI_PROJECT_DIR}/docker/blockchain/block_log" "${DATADIR}/blockchain/block_log" + sudo cp -a "${CI_PROJECT_DIR}/docker/blockchain/block_log.artifacts" "${DATADIR}/blockchain/block_log.artifacts" + + # Save to local cache + LOCAL_APP_CACHE="${DATA_CACHE_HAF_PREFIX}_${APP_SYNC_CACHE_TYPE}_${APP_CACHE_KEY}" + mkdir -p "${LOCAL_APP_CACHE}" + sudo cp -a "${DATADIR}" "${LOCAL_APP_CACHE}" + + ls -lah "${LOCAL_APP_CACHE}" + ls -lah "${LOCAL_APP_CACHE}/datadir" || true + + # Push to NFS cache + if [[ ! -x "$CACHE_MANAGER" ]]; then + curl -fsSL "https://gitlab.syncad.com/hive/common-ci-configuration/-/raw/${CACHE_MANAGER_REF:-develop}/scripts/cache-manager.sh" -o "$CACHE_MANAGER" + chmod +x "$CACHE_MANAGER" + fi + + echo "Pushing sync data to NFS: ${APP_SYNC_CACHE_TYPE}/${APP_CACHE_KEY}" + if CACHE_HANDLING=haf "$CACHE_MANAGER" put "${APP_SYNC_CACHE_TYPE}" "${APP_CACHE_KEY}" "${LOCAL_APP_CACHE}"; then + echo "Successfully pushed cache to NFS" + else + echo "WARNING: Failed to push to NFS cache" + fi + echo -e "\e[0Ksection_end:$(date +%s):save_cache\r\e[0K" + +# Cleanup job directory in after_script +# Use: after_script: !reference [.haf_app_sync_cleanup, after_script] +.haf_app_sync_cleanup: + after_script: + - | + echo -e "\e[0Ksection_start:$(date +%s):cleanup[collapsed=true]\r\e[0KCleaning up..." + sudo rm -rf ${CI_PROJECT_DIR}/${CI_JOB_ID} 2>/dev/null || true + echo -e "\e[0Ksection_end:$(date +%s):cleanup\r\e[0K" + +# Standard artifacts for sync jobs +# Use: artifacts: !reference [.haf_app_sync_artifacts, artifacts] +.haf_app_sync_artifacts: + artifacts: + paths: + - docker/container-logs.tar.gz + expire_in: 1 week + when: always + +# ============================================================================= +# CLEANUP TEMPLATES +# ============================================================================= +# Manual cleanup jobs for cache data + +.haf_app_cache_cleanup: + extends: .job-defaults + stage: cleanup + image: alpine:latest + variables: + HAF_APP_CACHE_TYPE: "" # Must be set + CLEANUP_PATTERN: "" # Optional glob pattern + before_script: + - apk add --no-cache bash + script: + - | + set -euo pipefail + + echo "=== Cache Cleanup ===" + echo "Cache type: ${HAF_APP_CACHE_TYPE}" + echo "Local prefix: ${DATA_CACHE_LOCAL_PREFIX}" + + # Build pattern + if [[ -n "${CLEANUP_PATTERN}" ]]; then + PATTERN="${DATA_CACHE_LOCAL_PREFIX}/${CLEANUP_PATTERN}" + else + PATTERN="${DATA_CACHE_LOCAL_PREFIX}/${HAF_APP_CACHE_TYPE}_*" + fi + + echo "Cleanup pattern: $PATTERN" + echo "" + echo "Files to remove:" + ls -la $PATTERN 2>/dev/null || echo "(none found)" + + echo "" + echo "Removing..." + rm -rf $PATTERN || true + echo "Done" + when: manual + allow_failure: true + tags: + - data-cache-storage