Tech Notes: SWLA CI/CD Pipeline

The SWLA Records platform is a Flask application backed by MySQL on GoDaddy, deployed to Render.com with a GitHub Actions CI/CD pipeline. This post documents the deployment architecture and the pipeline setup — useful reference for anyone running a similar stack.

Stack overview

Backend
Flask + SQLAlchemy + PyMySQL
Frontend
Vanilla JS + Jinja2 + CSS
Database
MySQL on GoDaddy (swla_dev / swla_prod)
Hosting
Render.com (Web Service)
CI/CD
GitHub Actions
Repo
blackrageous/swla_repos

How the pipeline works

Every push to the main branch triggers a GitHub Actions workflow that runs tests and — if they pass — signals Render.com to pull the latest code and redeploy. Render handles the build, dependency installation, and service restart automatically. The two database environments (swla_dev and swla_prod) are kept separate via environment variables injected at runtime, never committed to the repo.

GitHub Actions workflow

# .github/workflows/deploy.yml name: Deploy to Render on: push: branches: – main jobs: deploy: runs-on: ubuntu-latest steps: – name: Checkout code uses: actions/checkout@v3 – name: Set up Python uses: actions/setup-python@v4 with: python-version: ‘3.11’ – name: Install dependencies run: pip install -r requirements.txt – name: Trigger Render deploy run: | curl -X POST \ -H “Authorization: Bearer ${{ secrets.RENDER_API_KEY }}” \ “https://api.render.com/v1/services/${{ secrets.RENDER_SERVICE_ID }}/deploys”
Key point: The Render API key and service ID are stored as GitHub repository secrets (Settings → Secrets and variables → Actions) — never hardcoded. The same pattern applies to DB_HOST, DB_USER, DB_PASSWORD, and DB_PORT, which Render injects as environment variables at runtime.

Environment variable management

The Flask app reads its database connection from environment variables using a .env file locally and Render’s environment variable dashboard in production. The --env flag on the import and linking scripts selects between swla_dev and swla_prod at runtime, making it safe to test destructive operations (like a full re-link run) in dev before touching prod.

# Example: run linker against dev only python scripts/link_hebert_census.py –env dev –race B MU # Push to prod only after dev results are satisfactory python scripts/link_hebert_census.py –env prod –race B MU

Render.com cold starts

On Render’s free tier, web services spin down after 15 minutes of inactivity and take 10–30 seconds to wake on the next request. For a research tool with intermittent traffic this is acceptable. The startup screen (“Service waking up…”) is shown to users while the container initializes. Upgrading to a paid Render instance eliminates cold starts if response time becomes a priority.


Python Flask GitHub Actions CI/CD Render.com MySQL SQLAlchemy

SWLA RECORDS PLATFORM

For most people researching Black and Creole family history in Louisiana, 1865 is a wall. Before emancipation, records become sparse, names disappear, and the paper trail that genealogists depend on simply stops. The question of who your family was before the Civil War — where they lived, what they were called, who baptized them — has been effectively unanswerable for generations of researchers.

The Louisiana Heritage Platform is an attempt to break that wall.


Three archives, one platform

The platform links three independent historical archives spanning 1719 to 1880, covering notarial acts, Catholic sacramental records, and US Census data for Louisiana. Together they contain over 1.4 million records — and more importantly, they overlap in ways that allow individuals to be traced across them.

Archive Years Records Content
Hall Archive 1719–1820 104,729 Notarial acts, sales, manumissions, inventories
Hebert Archive 1720–1865 390,687 Catholic parish sacramental records — baptisms, marriages, burials
1880 US Census 1880 935,068 Louisiana households — race, age, occupation, birthplace, parish
Total 1719–1880 1,430,484
1.4M+ Total records across three archives
6,176 Confirmed Hall ↔ Hebert cross-archive links
161 years Span of coverage, 1719 to 1880
1865 The barrier this platform is built to break

Cross-archive linking

The core of the platform isn’t the records themselves — it’s the links between them. A person baptized in the Hebert archive in 1830 might appear in a Hall notarial act in 1818 and in the 1880 Census as an elderly head of household. Connecting those three appearances requires matching across different record types, spelling variations, parish geographies, and time gaps measured in decades.

The linking algorithm scores candidate matches on name similarity (using Soundex and given name comparison), parish geography, and year proximity. Matches scoring 90% or above are flagged as strong; 65–89% as possible. The Hall ↔ Hebert linker is live in production with 6,176 confirmed links. The Hebert ↔ Census linker has produced 1,515 links in development and is being tuned before a production push.


Who it’s for

The platform is designed for genealogical researchers working on Black and Creole family history in Louisiana — specifically those whose research hits the pre-emancipation wall. The Hebert archive’s Catholic sacramental records are particularly valuable because they documented enslaved individuals and free people of color by name, decades before emancipation-era records begin.

The site is currently password-protected while development continues. If you’re a researcher interested in access, send an email to garland.joseph@gmail.com.


Visit the Louisiana Heritage Platform at swla-records.garlandjoseph.org

Open SWLA Records →

Genealogy Louisiana Black history Creole 1880 Census Hebert Archive Hall Archive

Census Calculator

Garland Joseph

This program will input a 4-digit year and then relate the generations of your ancestors to the particular United States Census

A Python utility for genealogical research. Input a birth year and number of generations — the calculator maps each ancestor to the US Census records they would have appeared in, accounting for the 72-year privacy rule, the 1890 fire, and pre-1850 head-of-household-only records.

census_calculator.py — interactive

Ancestral Census Map
Gen Relationship ~Birth Year First Census Age Notes
census_calculator.py
#Garland R. Joseph | garland.joseph@gmail.com
#October 10, 2025
# This program will input a 4-digit year and then relate
# the generations of your ancestors
# to the particular United States Census

import math
from datetime import datetime

def calculate_ancestral_census(birth_year, generations, generation_length=30):
    """
    Calculate ancestral census information based on birth year and generations.

    Args:
        birth_year (int): The birth year of the starting person
        generations (int): Number of generations to calculate back
        generation_length (int): Average years per generation (default: 30)

    Returns:
        list: List of dictionaries containing ancestral census data
    """

    current_year = datetime.now().year

    if birth_year < 1700 or birth_year > current_year:
        raise ValueError(f"Birth year must be between 1700 and {current_year}")

    if generations < 1 or generations > 10:
        raise ValueError("Generations must be between 1 and 10")

    if generation_length < 20 or generation_length > 40:
        raise ValueError("Generation length must be between 20 and 40 years")

    results = []

    for gen in range(1, generations + 1):
        ancestor_birth = birth_year - (gen * generation_length)
        first_census = math.ceil((ancestor_birth + 1) / 10) * 10
        age_in_census = first_census - ancestor_birth

        if gen == 1:
            relationship = "Parent"
        elif gen == 2:
            relationship = "Grandparent"
        elif gen == 3:
            relationship = "Great-Grandparent"
        else:
            relationship = f"{gen-1}x Great-Grandparent"

        notes = []
        if first_census < 1790:
            notes.append("Before first US census")
        elif first_census < 1850:
            notes.append("Pre-1850: Only head of household named")
        elif first_census == 1890:
            notes.append("1890 Census mostly destroyed by fire")
        elif first_census == 1950:
            notes.append("1950 Census is most recently available")
        elif first_census > 1950:
            notes.append("Census not yet publicly available (72-year rule)")

        results.append({
            'generation': gen,
            'relationship': relationship,
            'approx_birth_year': ancestor_birth,
            'first_census': first_census,
            'age_in_census': age_in_census,
            'notes': notes
        })

    return results


def display_results(birth_year, generations, generation_length, results):
    """Display the results in a formatted table."""
    print("\n" + "="*80)
    print(f"ANCESTRAL CENSUS CALCULATOR")
    print(f"Starting Birth Year: {birth_year}")
    print(f"Generations: {generations} | Generation Length: {generation_length} years")
    print("="*80)
    print(f"\n{'Gen':<4} {'Relationship':<20} {'Approx Birth':<14} {'First Census':<12} {'Age':<6} Notes")
    print("-" * 80)
    for result in results:
        notes = "; ".join(result['notes']) if result['notes'] else ""
        print(f"{result['generation']:<4} {result['relationship']:<20} {result['approx_birth_year']:<14} {result['first_census']:<12} {result['age_in_census']:<6} {notes}")


def main():
    """Main function to run the ancestral census calculator."""
    print("ANCESTRAL CENSUS CALCULATOR")
    print("Calculates when ancestors appear in US census records.")
    print("-" * 60)
    try:
        birth_year = int(input("Enter the starting birth year (e.g., 1962): "))
        generations = int(input("Enter number of generations to calculate (1-10): "))
        use_default = input("Use default 30-year generation length? (y/n): ").lower().strip()
        generation_length = 30 if use_default == 'y' else int(input("Enter generation length (20-40): "))
        results = calculate_ancestral_census(birth_year, generations, generation_length)
        display_results(birth_year, generations, generation_length, results)
    except ValueError as e:
        print(f"Error: {e}")


if __name__ == "__main__":
    main()

Important Notes

  • Censuses before 1850 only listed the head of household by name — individuals may not appear by name
  • The 1890 US Census was mostly destroyed in a 1921 fire — expect gaps
  • Census records are publicly available after 72 years — 1950 is the most recent available census
  • Birth year estimates are approximations — actual family patterns will vary
  • Default generation length is 30 years; adjust between 20–40 to match known family patterns