ci(drift): enforce alias freshness checks in CI and contributor flow (#2910)
Merging alias-drift guardrails and local hook hardening.
This commit is contained in:
6
.githooks/pre-commit
Executable file
6
.githooks/pre-commit
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if git diff --cached --name-only | grep -Eq "^sdk/src/query/command-manifest\.|^sdk/src/query/command-aliases\.generated\.ts$|^get-shit-done/bin/lib/command-aliases\.generated\.cjs$|^sdk/scripts/gen-command-aliases\.ts$"; then
|
||||
npm run check:alias-drift
|
||||
fi
|
||||
48
.githooks/pre-push
Executable file
48
.githooks/pre-push
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
zero_sha='0000000000000000000000000000000000000000'
|
||||
blocked_regex="${GSD_BLOCKED_AUTHOR_REGEX:-}"
|
||||
|
||||
# Local-only guard: no-op unless the developer opts in via env var, e.g.
|
||||
# export GSD_BLOCKED_AUTHOR_REGEX='@example-corp\.com$'
|
||||
if [[ -z "$blocked_regex" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
violations=()
|
||||
|
||||
while read -r local_ref local_sha remote_ref remote_sha; do
|
||||
# branch/tag deletion
|
||||
if [[ "$local_sha" == "$zero_sha" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$remote_sha" == "$zero_sha" ]]; then
|
||||
# New remote ref: inspect commits not already on any remote
|
||||
commit_list=$(git rev-list "$local_sha" --not --remotes)
|
||||
else
|
||||
commit_list=$(git rev-list "$remote_sha..$local_sha")
|
||||
fi
|
||||
|
||||
while read -r commit; do
|
||||
[[ -z "$commit" ]] && continue
|
||||
author_email=$(git show -s --format='%ae' "$commit")
|
||||
lower_email=$(printf '%s' "$author_email" | tr '[:upper:]' '[:lower:]')
|
||||
if printf '%s' "$lower_email" | grep -Eq "$blocked_regex"; then
|
||||
violations+=("$commit <$author_email>")
|
||||
fi
|
||||
done <<< "$commit_list"
|
||||
done
|
||||
|
||||
if [[ ${#violations[@]} -gt 0 ]]; then
|
||||
{
|
||||
echo "Push blocked: commit author email matched local blocked regex ($blocked_regex)."
|
||||
echo "Rewrite author info before pushing these commits:"
|
||||
for v in "${violations[@]}"; do
|
||||
echo " - $v"
|
||||
done
|
||||
echo "Suggested fix: git rebase -i <base> --exec \"git commit --amend --no-edit --author='Your Name <non-enterprise@email>'\""
|
||||
} >&2
|
||||
exit 1
|
||||
fi
|
||||
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
@@ -88,6 +88,18 @@ jobs:
|
||||
- name: Build SDK dist (required by installer)
|
||||
run: npm run build:sdk
|
||||
|
||||
# Seam contract gate: keep manifest -> generated aliases -> registry/CJS adapters aligned.
|
||||
# Run once per workflow on the primary Linux node to avoid redundant matrix cost.
|
||||
- name: SDK seam coverage tests
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24
|
||||
shell: bash
|
||||
run: cd sdk && npx vitest run src/query/command-seam-coverage.test.ts
|
||||
|
||||
- name: SDK generated alias artifact drift check
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24
|
||||
shell: bash
|
||||
run: node sdk/scripts/check-command-aliases-fresh.mjs
|
||||
|
||||
- name: Run tests with coverage
|
||||
shell: bash
|
||||
run: npm run test:coverage
|
||||
|
||||
@@ -345,6 +345,73 @@ node --test tests/core.test.cjs
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
### Pre-PR Seam Checks (Manifest/Alias Routing)
|
||||
|
||||
If you touched any of the command-manifest or generated alias files, run:
|
||||
|
||||
```bash
|
||||
npm run check:alias-drift
|
||||
```
|
||||
|
||||
This verifies generated alias artifacts are in sync with manifest source-of-truth.
|
||||
|
||||
Optional local pre-commit hook entry (Git-native):
|
||||
|
||||
```bash
|
||||
# one-time setup
|
||||
mkdir -p .githooks
|
||||
cat > .githooks/pre-commit <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if git diff --cached --name-only | grep -Eq "^sdk/src/query/command-manifest\.|^sdk/src/query/command-aliases\.generated\.ts$|^get-shit-done/bin/lib/command-aliases\.generated\.cjs$|^sdk/scripts/gen-command-aliases\.ts$"; then
|
||||
npm run check:alias-drift
|
||||
fi
|
||||
EOF
|
||||
chmod +x .githooks/pre-commit
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
Optional local pre-push hook to block a private author-email pattern:
|
||||
|
||||
```bash
|
||||
# set locally in your shell profile (example)
|
||||
export GSD_BLOCKED_AUTHOR_REGEX='@example-corp\\.com$'
|
||||
|
||||
cat > .githooks/pre-push <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
zero_sha='0000000000000000000000000000000000000000'
|
||||
blocked_regex="${GSD_BLOCKED_AUTHOR_REGEX:-}"
|
||||
[[ -z "$blocked_regex" ]] && exit 0
|
||||
violations=()
|
||||
|
||||
while read -r local_ref local_sha remote_ref remote_sha; do
|
||||
[[ "$local_sha" == "$zero_sha" ]] && continue
|
||||
if [[ "$remote_sha" == "$zero_sha" ]]; then
|
||||
commits=$(git rev-list "$local_sha" --not --remotes)
|
||||
else
|
||||
commits=$(git rev-list "$remote_sha..$local_sha")
|
||||
fi
|
||||
while read -r commit; do
|
||||
[[ -z "$commit" ]] && continue
|
||||
email=$(git show -s --format='%ae' "$commit" | tr '[:upper:]' '[:lower:]')
|
||||
if printf '%s' "$email" | grep -Eq "$blocked_regex"; then
|
||||
violations+=("$commit <$email>")
|
||||
fi
|
||||
done <<< "$commits"
|
||||
done
|
||||
|
||||
if [[ ${#violations[@]} -gt 0 ]]; then
|
||||
echo "Push blocked: commit author email matched local blocked regex ($blocked_regex)." >&2
|
||||
printf ' - %s\n' "${violations[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
EOF
|
||||
chmod +x .githooks/pre-push
|
||||
```
|
||||
|
||||
### CI Test Quality Checks
|
||||
|
||||
The following checks run on every PR in addition to the test suite:
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"scripts": {
|
||||
"build:hooks": "node scripts/build-hooks.js",
|
||||
"build:sdk": "cd sdk && npm ci && npm run build",
|
||||
"check:alias-drift": "cd sdk && npm run check:alias-drift",
|
||||
"prepublishOnly": "npm run build:hooks && npm run build:sdk",
|
||||
"pretest": "npm run build:sdk",
|
||||
"pretest:coverage": "npm run build:sdk",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"check:alias-drift": "npm run build && node scripts/check-command-aliases-fresh.mjs",
|
||||
"prepublishOnly": "rm -rf dist && tsc && chmod +x dist/cli.js",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run --project unit",
|
||||
|
||||
67
tests/precommit-alias-drift-hook.test.cjs
Normal file
67
tests/precommit-alias-drift-hook.test.cjs
Normal file
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const HOOK_PATH = path.join(ROOT, '.githooks', 'pre-commit');
|
||||
|
||||
function writeExec(filePath, content) {
|
||||
fs.writeFileSync(filePath, content, { mode: 0o755 });
|
||||
}
|
||||
|
||||
describe('.githooks/pre-commit alias drift guard', () => {
|
||||
test('runs npm check when staged files include command-manifest/alias artifacts', (t) => {
|
||||
const tmpDir = createTempDir('gsd-precommit-hook-');
|
||||
t.after(() => cleanup(tmpDir));
|
||||
|
||||
const binDir = path.join(tmpDir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
writeExec(path.join(binDir, 'git'), `#!/usr/bin/env bash\nprintf "%s\\n" "${'sdk/src/query/command-manifest.phase.ts'}"\n`);
|
||||
writeExec(path.join(binDir, 'npm'), `#!/usr/bin/env bash\nprintf "called" > "$GSD_TEST_NPM_MARKER"\n`);
|
||||
|
||||
const marker = path.join(tmpDir, 'npm-called.txt');
|
||||
|
||||
execFileSync('bash', [HOOK_PATH], {
|
||||
cwd: ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${binDir}:${process.env.PATH}`,
|
||||
GSD_TEST_NPM_MARKER: marker,
|
||||
},
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
assert.ok(fs.existsSync(marker), 'expected npm run check:alias-drift to be invoked');
|
||||
});
|
||||
|
||||
test('does not run npm check when staged files are unrelated', (t) => {
|
||||
const tmpDir = createTempDir('gsd-precommit-hook-');
|
||||
t.after(() => cleanup(tmpDir));
|
||||
|
||||
const binDir = path.join(tmpDir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
writeExec(path.join(binDir, 'git'), `#!/usr/bin/env bash\nprintf "%s\\n" "README.md"\n`);
|
||||
writeExec(path.join(binDir, 'npm'), `#!/usr/bin/env bash\nprintf "called" > "$GSD_TEST_NPM_MARKER"\n`);
|
||||
|
||||
const marker = path.join(tmpDir, 'npm-called.txt');
|
||||
|
||||
execFileSync('bash', [HOOK_PATH], {
|
||||
cwd: ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${binDir}:${process.env.PATH}`,
|
||||
GSD_TEST_NPM_MARKER: marker,
|
||||
},
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
assert.ok(!fs.existsSync(marker), 'expected npm check to be skipped for unrelated staged files');
|
||||
});
|
||||
});
|
||||
90
tests/prepush-enterprise-email-hook.test.cjs
Normal file
90
tests/prepush-enterprise-email-hook.test.cjs
Normal file
@@ -0,0 +1,90 @@
|
||||
'use strict';
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const HOOK_PATH = path.join(ROOT, '.githooks', 'pre-push');
|
||||
|
||||
function writeExec(filePath, content) {
|
||||
fs.writeFileSync(filePath, content, { mode: 0o755 });
|
||||
}
|
||||
|
||||
describe('.githooks/pre-push enterprise email guard', () => {
|
||||
test('blocks push when any to-be-pushed commit matches local blocked regex', (t) => {
|
||||
const tmpDir = createTempDir('gsd-prepush-hook-');
|
||||
t.after(() => cleanup(tmpDir));
|
||||
|
||||
const binDir = path.join(tmpDir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
writeExec(path.join(binDir, 'git'), `#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ "$1" == "rev-list" ]]; then
|
||||
echo "c1"
|
||||
echo "c2"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$1" == "show" ]]; then
|
||||
commit="$(printf '%s\n' "$@" | tail -n 1)"
|
||||
if [[ "$commit" == "c1" ]]; then
|
||||
echo "trekkie@nomorestars.com"
|
||||
else
|
||||
echo "person@example-corp.com"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
`);
|
||||
|
||||
assert.throws(() => {
|
||||
execFileSync('bash', [HOOK_PATH], {
|
||||
cwd: ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${binDir}:${process.env.PATH}`,
|
||||
GSD_BLOCKED_AUTHOR_REGEX: '@example-corp\\.com$',
|
||||
},
|
||||
input: 'refs/heads/pr refs-local-sha refs/heads/pr refs-remote-sha\n',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
}, /Push blocked: commit author email matched local blocked regex/);
|
||||
});
|
||||
|
||||
test('allows push when to-be-pushed commits are non-enterprise emails', (t) => {
|
||||
const tmpDir = createTempDir('gsd-prepush-hook-');
|
||||
t.after(() => cleanup(tmpDir));
|
||||
|
||||
const binDir = path.join(tmpDir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
writeExec(path.join(binDir, 'git'), `#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ "$1" == "rev-list" ]]; then
|
||||
echo "c1"
|
||||
echo "c2"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$1" == "show" ]]; then
|
||||
echo "trekkie@nomorestars.com"
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
`);
|
||||
|
||||
execFileSync('bash', [HOOK_PATH], {
|
||||
cwd: ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${binDir}:${process.env.PATH}`,
|
||||
GSD_BLOCKED_AUTHOR_REGEX: '@example-corp\\.com$',
|
||||
},
|
||||
input: 'refs/heads/pr refs-local-sha refs/heads/pr refs-remote-sha\n',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user