tbdflow 0.27.0

A CLI to streamline your Git workflow for Trunk-Based Development.
Documentation
# ═══════════════════════════════════════════════════════════════════════════════
# tbdflow — Standardised Commits
# ═══════════════════════════════════════════════════════════════════════════════
# User Story: As a developer, I want my commits to follow a standardised
# format so that changelogs and release notes can be generated automatically.
# ═══════════════════════════════════════════════════════════════════════════════

feature "tbdflow Standardised Commits"

actors: Terminal

settings {
    timeout_seconds = 30
    stop_on_failure = true
    shell_path = "/bin/bash"
}

# Each scenario gets its own isolated repo
var VALID_REPO = "/tmp/tbdflow_commit_valid"
var VALID_BARE = "/tmp/tbdflow_commit_valid_bare.git"
var INVALID_REPO = "/tmp/tbdflow_commit_invalid"
var INVALID_BARE = "/tmp/tbdflow_commit_invalid_bare.git"

# ═══════════════════════════════════════════════════════════════════════════════
# IMPLEMENTATION LAYER — Tasks hide the "how"
# ═══════════════════════════════════════════════════════════════════════════════

# --- Setup drivers ---

task init_repo(repo, bare) {
    Terminal run "rm -rf ${repo} ${bare} && mkdir -p ${repo}"
    Terminal run "git init --bare ${bare}"
    Terminal run "git -C ${repo} init && git -C ${repo} commit --allow-empty -m 'init'"
    Terminal run "git -C ${repo} remote add origin ${bare}"
    Terminal run "git -C ${repo} push -u origin main"
    Terminal set_cwd "${repo}"
}

task cleanup_repo(repo, bare) {
    Terminal run "rm -rf ${repo} ${bare}"
}

# --- Action drivers (for when: blocks) ---

task make_change(filename, content) {
    Terminal run "echo ${content} > ${filename}"
}

task commit(type, message) {
    Terminal run "tbdflow commit -t ${type} -m '${message}'"
}

task commit_scoped(type, scope, message) {
    Terminal run "tbdflow commit -t ${type} -s ${scope} -m '${message}'"
}

task commit_breaking(type, scope, message) {
    Terminal run "tbdflow commit -t ${type} -s ${scope} -m '${message}' -b"
}

task commit_with_issue(type, message, issue) {
    Terminal run "tbdflow commit -t ${type} -m '${message}' --issue ${issue}"
}

task sync() {
    Terminal run "tbdflow sync"
}

# --- Condition drivers (for then: blocks) ---

task verify_commit_succeeded() {
    Terminal last_command succeeded
    Terminal output_contains "Successfully committed"
    Terminal output_not_contains "Error"
}

task verify_commit_rejected() {
    Terminal last_command failed
    Terminal output_not_contains "Successfully committed"
}

task verify_history_contains(expected_message) {
    Terminal last_command succeeded
    Terminal output_contains "${expected_message}"
}

# ═══════════════════════════════════════════════════════════════════════════════
# BUSINESS SPECIFICATION LAYER — Scenarios describe the "what"
# ═══════════════════════════════════════════════════════════════════════════════

# ─────────────────────────────────────────────────────────────────────────────
# AC1: Commits must use conventional types (feat, fix, chore, etc.).
# AC2: Commits may include an optional scope.
# AC3: Commits may reference an issue key.
# AC4: Breaking changes must be flagged explicitly.
# ─────────────────────────────────────────────────────────────────────────────

scenario "Creating valid conventional commits" {

    test Setup "Initialise test repository" {
        given:
            Test can_start
        when:
            init_repo("${VALID_REPO}", "${VALID_BARE}")
        then:
            Terminal last_command succeeded
    }

    test SimpleFeat "Commit a simple feature" {
        given:
            Test has_succeeded Setup
        when:
            make_change("new_feature.txt", "hello")
            commit("feat", "add user greeting")
        then:
            verify_commit_succeeded()
    }

    test VerifyFeatCommit "The commit message follows conventional format" {
        given:
            Test has_succeeded SimpleFeat
        when:
            sync()
        then:
            verify_history_contains("feat: add user greeting")
    }

    test ScopedFix "Commit a scoped fix" {
        given:
            Test has_succeeded VerifyFeatCommit
        when:
            make_change("bugfix.txt", "patched")
            commit_scoped("fix", "auth", "resolve token expiry")
        then:
            Terminal last_command succeeded
    }

    test VerifyScopedCommit "The scoped commit appears in the log" {
        given:
            Test has_succeeded ScopedFix
        when:
            sync()
        then:
            verify_history_contains("fix(auth): resolve token expiry")
    }

    test BreakingChange "Commit with breaking change flag" {
        given:
            Test has_succeeded VerifyScopedCommit
        when:
            make_change("api.txt", "v2")
            commit_breaking("feat", "api", "remove legacy endpoint")
        then:
            Terminal last_command succeeded
    }

    test CommitWithIssue "Commit referencing an issue key" {
        given:
            Test has_succeeded BreakingChange
        when:
            make_change("ticket.txt", "ticket_work")
            commit_with_issue("chore", "update dependencies", "PROJ-123")
        then:
            verify_commit_succeeded()
    }

    after {
        cleanup_repo("${VALID_REPO}", "${VALID_BARE}")
    }
}

# ─────────────────────────────────────────────────────────────────────────────
# AC5: Invalid commit types must be rejected.
# AC6: Commit messages starting with a capital letter must be rejected.
# AC7: Commit messages ending with a period must be rejected.
# ─────────────────────────────────────────────────────────────────────────────

scenario "Rejecting invalid commit messages" {

    test Setup "Initialise test repository" {
        given:
            Test can_start
        when:
            init_repo("${INVALID_REPO}", "${INVALID_BARE}")
        then:
            Terminal last_command succeeded
    }

    test InvalidType "Reject an unknown commit type" {
        given:
            Test has_succeeded Setup
        when:
            make_change("tmp.txt", "x")
            commit("feature", "add something")
        then:
            verify_commit_rejected()
    }

    test CapitalisedMessage "Reject a message starting with a capital letter" {
        given:
            Test has_succeeded InvalidType
        when:
            make_change("tmp2.txt", "x")
            commit("fix", "Fix the bug")
        then:
            verify_commit_rejected()
    }

    test TrailingPeriod "Reject a message ending with a period" {
        given:
            Test has_succeeded CapitalisedMessage
        when:
            make_change("tmp3.txt", "x")
            commit("fix", "fix the bug.")
        then:
            verify_commit_rejected()
    }

    after {
        cleanup_repo("${INVALID_REPO}", "${INVALID_BARE}")
    }
}