Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 2
- package-ecosystem: pip
directory: /.github
schedule:
interval: weekly
open-pull-requests-limit: 1
13 changes: 13 additions & 0 deletions .github/fixtures/s01.expected.psv
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
O01|Growth Engineer
O02|Growth Lead
O03|MarTech Engineer
O04|SEO Operations
O05|Growth Engineer
O01|Growth Engineer|direct|2026-09-01 08:00:00+00
O02|Growth Lead|job_board|2026-09-02 08:00:00+00
O03|MarTech Engineer|referral|2026-09-03 08:00:00+00
O01|Growth Engineer|direct|2026-09-01 08:00:00+00
O02|Growth Lead|job_board|2026-09-02 08:00:00+00
O03|MarTech Engineer|referral|2026-09-03 08:00:00+00
O04|SEO Operations|job_board|2026-09-04 08:00:00+00
O05|Growth Engineer|direct|2026-09-05 08:00:00+00
1 change: 1 addition & 0 deletions .github/requirements-ci.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PyYAML==6.0.3
144 changes: 144 additions & 0 deletions .github/scripts/check_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""Check content changes; report existing scaffolding without claiming it works.

This checks syntax and local file links, not source accuracy, schema semantics,
the quality of a growth method, or whether a Python application is implemented.
"""

import argparse
import ast
import json
import os
from pathlib import Path
import re
import subprocess
import sys
import tomllib
from urllib.parse import unquote, urlsplit

import yaml


TEXT_SUFFIXES = {'.md', '.json', '.yaml', '.yml', '.toml', '.py', '.sql', '.j2', '.psv'}
INLINE_LINK = re.compile(r'!?\[[^\]\n]*\]\(\s*(<[^>\n]+>|[^\s)]+)(?:\s+[\"\'][^\n]*?[\"\'])?\s*\)')


def git(root, *args):
return subprocess.check_output(['git', '-C', str(root), *args])


def inspect_file(root, name):
path = root / name
issues = []
if any(part != part.strip() for part in Path(name).parts):
issues.append('file or directory name has leading/trailing whitespace')
if path.suffix.lower() not in TEXT_SUFFIXES:
return issues
try:
text = path.read_text(encoding='utf-8')
if not text.strip():
if path.name != '__init__.py':
issues.append('empty placeholder; not a completed implementation')
return issues
if path.suffix == '.json':
json.loads(text)
elif path.suffix in {'.yaml', '.yml'}:
yaml.safe_load(text)
elif path.suffix == '.toml':
tomllib.loads(text)
elif path.suffix == '.py':
ast.parse(text, filename=name)
elif path.suffix == '.md':
# Avoid links in fenced examples. Check inline local file links only;
# external URLs, reference-style links and section anchors are not checked.
in_fence = False
fence_mark = None
prose = []
for line in text.splitlines():
match = re.match(r'^\s*(`{3,}|~{3,})', line)
if match:
mark = match.group(1)[0]
if not in_fence:
in_fence, fence_mark = True, mark
elif mark == fence_mark:
in_fence, fence_mark = False, None
continue
if not in_fence:
prose.append(line)
for match in INLINE_LINK.finditer('\n'.join(prose)):
target = match.group(1).strip('<>')
parts = urlsplit(target)
if parts.scheme or parts.netloc or not parts.path:
continue
# Root-relative links on GitHub can be site routes, not repo files.
if parts.path.startswith('/'):
continue
destination = path.parent / unquote(parts.path)
if not destination.exists():
issues.append('missing local link target: ' + target)
except (UnicodeError, ValueError, SyntaxError, yaml.YAMLError) as exc:
issues.append(str(exc))
return issues


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--root', default='.')
parser.add_argument('--base', default=os.environ.get('BASE_SHA', ''))
parser.add_argument('--org-profile', action='store_true')
args = parser.parse_args()
root = Path(args.root).resolve()
tracked = git(root, 'ls-files', '-z').decode().split('\0')[:-1]
tracked = [name for name in tracked if (root / name).is_file()]

required = ['README.md']
is_profile = args.org_profile or os.environ.get('GITHUB_REPOSITORY', '').endswith('/.github')
if is_profile:
required.append('profile/README.md')
failures = []
for name in required:
path = root / name
if not path.is_file() or not path.read_text(encoding='utf-8').strip():
failures.append(name + ': required published entry point is missing or empty')
elif not re.search(r'^#\s+\S', path.read_text(encoding='utf-8'), re.MULTILINE):
failures.append(name + ': add a top-level Markdown title')

base = args.base.strip()
if base and set(base) != {'0'}:
if not re.fullmatch(r'[0-9a-fA-F]{40,64}', base):
raise SystemExit('BASE_SHA must be a commit hash')
changed = set(git(root, 'diff', '--diff-filter=ACMR', '--name-only', '-z', base, 'HEAD').decode().split('\0'))
changed.discard('')
else:
changed = set()
print('Baseline/manual run: report existing scaffolding; required README entry points remain enforced.')

backlog = []
for name in tracked:
issues = inspect_file(root, name)
if issues:
entries = [name + ': ' + issue for issue in issues]
for issue, entry in zip(issues, entries):
# Planned scaffold files can remain empty or be moved. Only
# published README entry points must be nonempty at this stage.
if issue.startswith('empty placeholder') and name not in required:
backlog.append(entry)
elif name in changed or name in required:
failures.append(entry)
else:
backlog.append(entry)
if backlog:
print(f'Existing content backlog: {len(backlog)} issue(s). These are not functional-test passes.')
for item in backlog[:12]:
print('BACKLOG: ' + item)
if len(backlog) > 12:
print(f'... {len(backlog) - 12} more issue(s); run the script after each content update.')
if failures:
for item in failures:
print('FAIL: ' + item, file=sys.stderr)
return 1
print(f'Content checks passed: {len(changed)} changed path(s); published entry points verified.')
return 0


if __name__ == '__main__':
raise SystemExit(main())
47 changes: 47 additions & 0 deletions .github/scripts/check_s01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Execute the existing S01 SQL in an isolated PostgreSQL CI service.

Compare its three query outputs with the recorded synthetic-data results.
This proves reproducibility of the submitted SQL, not unaided SQL competence.
"""

import difflib
import os
from pathlib import Path
import subprocess
import sys


def main():
root = Path('.').resolve()
candidates = list(root.glob('use-cases/**/s01-opportunity-inventory/run.sql'))
if len(candidates) != 1:
raise SystemExit(f'Expected exactly one S01 run.sql, found {len(candidates)}. Update the CI path explicitly if the proof moves.')
expected = (root / '.github/fixtures/s01.expected.psv').read_text(encoding='utf-8').strip()
container = os.environ.get('POSTGRES_CONTAINER')
if not container:
raise SystemExit('POSTGRES_CONTAINER is required; this check needs an isolated PostgreSQL service.')
result = subprocess.run(
[
'docker', 'exec', '-i', '--env', 'PGOPTIONS=-c timezone=UTC',
container, 'psql', '--no-psqlrc', '--quiet', '--tuples-only',
'--no-align', '--field-separator', '|',
'--username', 'directgtm_ci', '--dbname', 'directgtm_ci',
'--set', 'ON_ERROR_STOP=1',
],
input=candidates[0].read_text(encoding='utf-8'),
text=True, capture_output=True, timeout=120,
)
if result.returncode:
print(result.stderr, file=sys.stderr)
return result.returncode
actual = result.stdout.strip()
if actual != expected:
print('\n'.join(difflib.unified_diff(expected.splitlines(), actual.splitlines(), fromfile='recorded S01 outputs', tofile='current SQL execution', lineterm='')), file=sys.stderr)
return 1
print('S01 passed: Q1 5x2, Q2 3x4, Q3 5x4; row order and values match the recorded outputs.')
print('Data: synthetic. Assisted status and independent competence remain separate records.')
return 0


if __name__ == '__main__':
raise SystemExit(main())
68 changes: 68 additions & 0 deletions .github/workflows/directgtm-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: DirectGTM CI

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: directgtm-ci-${{ github.ref }}
cancel-in-progress: true

jobs:
content-checks:
name: Content checks
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Install CI dependencies
run: python -m pip install -r .github/requirements-ci.txt
- name: Check published entry points and changed content
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: python .github/scripts/check_content.py

sql-proof:
name: S01 SQL proof
runs-on: ubuntu-24.04
timeout-minutes: 10
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: directgtm_ci
POSTGRES_DB: directgtm_ci
# Disposable synthetic-data CI database; this is not a production secret.
POSTGRES_PASSWORD: directgtm_ci_ephemeral
options: >-
--health-cmd "pg_isready -U directgtm_ci -d directgtm_ci"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Reproduce the recorded S01 query outputs
env:
POSTGRES_CONTAINER: ${{ job.services.postgres.id }}
run: python .github/scripts/check_s01.py