Sample Page

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

#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
    """
    
    # Get current year for validation
    current_year = datetime.now().year
    
    # Validate input
    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):
        # Calculate ancestor's approximate birth year
        ancestor_birth = birth_year - (gen * generation_length)
        
        # Calculate first census where they would appear as a child (age 0-9)
        # Using the formula: ROUNDUP((birth_year + 1) / 10) × 10
        first_census = math.ceil((ancestor_birth + 1) / 10) * 10
        
        # Calculate their age in that census
        age_in_census = first_census - ancestor_birth
        
        # Determine relationship label
        if gen == 1:
            relationship = "Parent"
        elif gen == 2:
            relationship = "Grandparent"
        elif gen == 3:
            relationship = "Great-Grandparent"
        else:
            relationship = f"{gen-1}x Great-Grandparent"
        
        # Add historical context notes
        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 to Calculate: {generations}")
    print(f"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("This program calculates when your ancestors would appear in US census records.")
    print("Default generation length is 30 years. US Censuses occur every 10 years (1790, 1800, etc.)")
    print("-" * 60)
    
    try:
        # Get user input
        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()
        if use_default == 'y':
            generation_length = 30
        else:
            generation_length = int(input("Enter generation length in years (20-40): "))
        
        # Calculate results
        results = calculate_ancestral_census(birth_year, generations, generation_length)
        
        # Display results
        display_results(birth_year, generations, generation_length, results)
        
        # Additional information
        print("\n" + "="*80)
        print("IMPORTANT NOTES:")
        print("- Censuses before 1850 only listed the head of household by name")
        print("- The 1890 US Census was mostly destroyed by fire")
        print("- Census records are publicly available after 72 years (1950 is latest)")
        print("- These are approximations - actual family patterns may vary")
        print("="*80)
        
    except ValueError as e:
        print(f"Error: {e}")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")

# Example usage function
def run_example():
    """Run an example with predefined values."""
    print("Running example for birth year 1962, 5 generations:")
    results = calculate_ancestral_census(1962, 5)
    display_results(1962, 5, 30, results)

if __name__ == "__main__":
    # Uncomment the next line to run an example instead of interactive input
    # run_example()
    
    main()

The Best Guitar Player in Texas

by gRj

They say you’re dying—
but the morning doesn’t know it,
still spilling gold through your window,
still begging for your rusty guitar.

You count your breaths like old guitar strings,
yet somehow—
there’s always one left
to hum the chorus of Wild Horses,
one more to pluck the notes
that slip like loose change
through your fingers,
but never quite vanish.

And if death is a door,
it’s stuck halfway open,
and you’re neither coming nor going—
just leaning on the jamb,
tuning that old six-string
to a key only the wind understands.

Men who are done with living
don’t name the birds at their feeders.
They don’t laugh when the rain
soaks through their boots,
or grin at the nurse
who calls them honey
and means it.

You measure time in coffee stains,
in snapped strings and calloused thumbs,
in the way your voice,
roughened to gravel,
still finds the high notes—
a hand groping for the light switch
in a familiar dark.

Dedicated to George in Austin

George’s Song

by gRj

For the man who carried home on his back,
and hummed his way through the dark—

You were a wildfire in a body
that wouldn’t burn slow—
lungs like paper lanterns,
still glowing where the wind tore holes.

The woods knew you first:
your tent a cathedral of rain,
your bicycle a steel-winged bird
that never left the ground.

(How many winters did you pedal,
wheels spinning hymns between your ribs,
chain rusted with the weight
of all the roads you couldn’t stay on?)

Then—miracles in small type:
four walls, a key, a car
that started when you turned it.
You rolled the windows down
just to feel the world move with you,
not past.

Now the emphysema comes
like a thief you’ve learned to name.
You laugh around the oxygen hose,
pluck your guitar till your fingers stiffen,
sing till the notes fray
into something too honest
to call music.

Oh, George,
they’ll never understand
how a man who’s been untethered
so long can strum the light
clean through the cracks—
your voice a stubborn knot
holding the sky to the tree line,
holding the road to its promise:
Forward. Always forward.

The Weave Remembers

There is a thread that runs through all,
Unseen, but older than the stars—
A sacred weave of soul and soil,
Of whispered truths and ancient scars.

It binds the humble to the high,
The farmer’s hand to banker’s pen,
The child’s cry to prophet’s sigh,
The now to all that’s ever been.

Some pull the thread for selfish weight,
Unraveling others to climb fast—
They weave with gold and trade in fate,
And think the fabric will not last.

But spirit does not sleep for long,
Nor turn its face from what is done.
The silent loom, though slow, is strong,
And justice spins for everyone.

The ones who feast while others break,
Who laugh while stealing from the poor—
Will find their riches turn to ache,
Their doors unguarded, threadbare, torn.

For every soul mistreated here,
A knot is tied in hidden space.
And when the wind of truth draws near,
The thread will find its rightful place.

So take what’s yours, but not what’s owed,
And walk the earth with open hands.
The weave remembers every load—
And rights the loom by its commands.

-gRj

Nothing New Under the Sun

by gRj

In a world where lies are laws: no regime, however oppressive, can erase the truth in the hearts of the people. The Party may control the present, but we, the free, control the past—and from it, the future. 

The regime thrives not by defeating its enemies, but by playing upon the differences between the people—keeping them isolated in their struggles, so they never see the power they could have if they stood united.

In the Civil War, the powerful used race to divide, convincing the poor to fight for the wealth of the few. The rich knew: as long as they could keep poor whites focused on differences, they would never see the true enemy—the system that oppressed them all.

During the Great Depression, economic hardship affected millions of Americans. However, politicians and business leaders, particularly during the 1930s, used scapegoating to divert blame for the economic crisis. Immigrants, African Americans, and other marginalized groups were often blamed for stealing jobs or resources.

During the Cold War, the U.S. government, under figures like Senator Joseph McCarthy, stoked fear of communism to suppress political dissent and labor movements. The government used anti-communist hysteria to label left-wing activists, labor leaders, and intellectuals as unpatriotic or even traitors, forcing many into silence or submission.

The War on Drugs has been another major example where race and class divisions were manipulated to create and maintain a criminal underclass. Although drug use is prevalent across all socioeconomic groups, law enforcement disproportionately targeted Black and Latino communities, leading to mass incarceration, particularly under the 1980s Reagan administration.

From the post-Reconstruction period to today, voter suppression has been a tactic used to limit the political power of certain groups, particularly African Americans, immigrants, and the poor. Practices like poll taxes, literacy tests, and modern voter ID laws disproportionately affect marginalized communities, making it harder for them to vote.

The labeling of political and economic systems such as capitalism, socialism, and communism has often been a method for framing debates in a way that fosters polarization. Instead of focusing on the actual outcomes of these systems, the labels themselves are used to define the value of the system—either as a shining example of human progress or as a dystopian nightmare.

Cold War Rhetoric: During the Cold War, for example, the U.S. government framed communism (especially the Soviet model) as an existential threat to freedom, democracy, and human dignity, while capitalism and the American system were depicted as the ultimate form of freedom and prosperity. This binary of “good vs. evil” made it easier to dismiss the complexities of both systems and keep citizens in a constant state of ideological opposition.

Political Polarization: In many countries, political elites use the terms “socialist” and “communist” as a form of fearmongering. In the U.S., for example, calling someone a “socialist” can evoke a visceral negative reaction, even if their policies or ideas do not align with traditional socialist thought. Conversely, in other parts of the world, capitalism is painted as the enemy of the people, blamed for economic inequality and exploitation.

George Orwell in 2025

by gRj

There are those who see the world unravel,

who feel the weight of truth in their bones.

They reach for others—not out of duty,

but because the soul remembers we are not meant to stand alone.

There are those who see as well,

but turn away.

They build walls from fear and profit,

calling it wisdom.

Their eyes are mirrors of their own desire.

And then—

there are the many caught between,

not heartless, but hidden from the light.

Told what to fear, taught whom to blame,

they march to songs not of their making.

This is the shape of our time:

The caring,

the cunning,

and the carried.

But lines are not cages.

A whisper of truth, a flicker of love,

and even the deepest sleeper can wake.

-by gRj