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
2 changes: 1 addition & 1 deletion .github/workflows/conventions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ on:
pull_request:
jobs:
conventions:
uses: robertblust/conventions/.github/workflows/check.yml@v1.43.0
uses: robertblust/conventions/.github/workflows/check.yml@v1.44.0
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- conventions · v1.43.0 -->
<!-- conventions · v1.44.0 -->
Shared conventions of the robertblust, guestgraph and companygraph organizations live in `conventions/`, vendored from robertblust/conventions at the release `conventions.json` names. Read them before writing or committing anything here.

- `conventions/WRITING.md` — how we write: one voice, three registers, English and German.
Expand Down
2 changes: 1 addition & 1 deletion conventions.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ "repo": "robertblust/conventions", "tag": "v1.43.0" }
{ "repo": "robertblust/conventions", "tag": "v1.44.0" }
2 changes: 1 addition & 1 deletion conventions/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- conventions · v1.43.0 -->
<!-- conventions · v1.44.0 -->
Shared conventions of the robertblust, guestgraph and companygraph organizations live in `conventions/`, vendored from robertblust/conventions at the release `conventions.json` names. Read them before writing or committing anything here.

- `conventions/WRITING.md` — how we write: one voice, three registers, English and German.
Expand Down
6 changes: 6 additions & 0 deletions conventions/PINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,9 @@ A member declares its pins in `pins.json` at its root. The report reads every pi
`verify` lists the commands that run once every pin of the member has moved, and a failing one blocks the member. A check in `verify`, `npm run <name>:check`, fails as soon as a move changes what its writer `npm run <name>` writes, so the report names a check whose writer no pin's `after` or `move` runs. `release` lists the commands that run when the resync releases the member, with `{version}` standing for the new version without its `v`; they leave the bump uncommitted, and the run commits it. A member that nothing takes by tag has no `release`.

The report names every entry that does not match a line in the file it names, so a `pins.json` that has fallen behind its member is seen the first time the report runs.

## Dependabot

A pin moves when the owner decides, in the resync, together with everything its move rewrites, so no bot proposes one. A bot that does moves the line alone: a conventions workflow past `conventions.json` and its vendored copy, a package past the files its `after` writes. Where a member keeps `.github/dependabot.yml`, each of its blocks ignores every family repository it would otherwise watch, and Dependabot keeps every other dependency in view. What a block watches is read from the member's own files in the block's directory: the `uses:` lines under `.github/workflows` for `github-actions`, a `github:owner/repo#tag` value in a `package.json` for `npm`, and a `git::` module source in a `.tf` file for `terraform`. A `dependency-name` is matched as a glob, so `"*robertblust/conventions*"` covers every workflow of the repository it names. A tool whose version a conventions release pins counts the same wherever a `package.json` declares it: `markdownlint-cli2`, which `conventions-format` runs as the family's Markdown form, moves with the release and not with a bot.

`conventions-check` holds this and names each family pin a block watches with the line to add to its ignore. A member whose `dependabot.yml` is itself vendored, as a service's is from guestgraph/service-conventions, takes the entry from where the file comes from.
97 changes: 96 additions & 1 deletion conventions/conventions-check
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,104 @@ else
fi
fi

if [ -n "$hits" ] || [ -n "$title_hit" ]; then
# --- Dependabot leaves the family's pins alone -------------------------------------------------
# A pin is editorial and the family resync moves it, with everything its move rewrites: a bot
# that proposes one moves a line alone, a conventions workflow past conventions.json, a package
# past what its `after` writes. So where a member keeps .github/dependabot.yml, every family
# repository a block of it would watch is ignored in that block. What a block watches is read
# from the member's own files in the block's directory: `uses:` lines under .github/workflows for
# github-actions, `github:owner/repo#` values in a package.json for npm, and `git::` module
# sources in a .tf file for terraform. A dependency-name is a glob, so "*companygraph/mcp-server*"
# covers every path of that repository. No file, no list, or no family pin is a quiet pass.
# The tools a conventions release pins count as family pins too, wherever a package.json declares
# them: their version is the family's, set in the vendored scripts, so a bump of one copy alone
# leaves the others behind. TOOLS is that list, and test/run.sh holds it to conventions-format.
BOT=$ROOT/.github/dependabot.yml
TOOLS="markdownlint-cli2"
bot_hits=""
if [ -f "$BOT" ] && [ -f "$LIST" ]; then
tab=$(printf '\t')
family=$(awk -F'|' '/^\|/ { n = $2; gsub(/^[ \t]+|[ \t]+$/, "", n); if (n ~ /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/) printf "%s ", n }' "$LIST")
# Every block as "ecosystem<TAB>directory<TAB>pattern" lines, one per ignored name, and one
# with no pattern, so a block that ignores nothing is still seen as watching its directory.
blocks=$(awk '
function val(s) { sub(/^[^:]*:[ \t]*/, "", s); sub(/[ \t]+#.*$/, "", s); gsub(/["\047]/, "", s); sub(/[ \t]+$/, "", s); return s }
function emit( i, j, d) {
if (eco == "") return
for (i = 1; i <= nd; i++) {
d = dirs[i]; sub(/\/+$/, "", d); if (d == "") d = "/"
print eco "\t" d "\t"
for (j = 1; j <= np; j++) print eco "\t" d "\t" pats[j]
}
}
/^[ \t]*-[ \t]*package-ecosystem:/ { emit(); eco = val($0); nd = 0; np = 0; inlist = 0; next }
/^[ \t]*directory:/ { dirs[++nd] = val($0); inlist = 0; next }
/^[ \t]*directories:/ { inlist = 1; next }
inlist && /^[ \t]*-[ \t]*["\047]?\// { s = $0; sub(/^[ \t]*-[ \t]*/, "", s); gsub(/["\047]/, "", s); sub(/[ \t]+$/, "", s); dirs[++nd] = s; next }
/^[ \t]*[a-z-]+:/ { inlist = 0 }
/dependency-name:/ { pats[++np] = val($0) }
END { emit() }' "$BOT")
# Every family pin the member's files hold, as "ecosystem<TAB>directory<TAB>name<TAB>where<TAB>suggestion".
dirof() { d=${1%/*}; d=${d#"$ROOT"}; [ -n "$d" ] || d=/; printf '%s' "$d"; }
pins=$(
for f in "$ROOT"/.github/workflows/*.yml "$ROOT"/.github/workflows/*.yaml; do
[ -f "$f" ] || continue
awk -v fam="$family" -v file="${f#"$ROOT"/}" '
BEGIN { n = split(fam, F, " "); for (i = 1; i <= n; i++) R[F[i]] = 1 }
/^[ \t]*#/ { next }
/uses:[ \t]*[^ \t]+@/ {
s = $0; sub(/.*uses:[ \t]*/, "", s); gsub(/["\047]/, "", s); sub(/@.*/, "", s)
split(s, p, "/"); r = p[1] "/" p[2]
if (r in R) print "github-actions\t/\t" s "\t" file ":" NR "\t*" r "*"
}' "$f"
done
find "$ROOT" -name node_modules -prune -o -name .git -prune -o -type f -name package.json -print | while IFS= read -r f; do
awk -v fam="$family" -v tools="$TOOLS" -v file="${f#"$ROOT"/}" -v dir="$(dirof "$f")" '
BEGIN { n = split(fam, F, " "); for (i = 1; i <= n; i++) R[F[i]] = 1; m = split(tools, T, " ") }
{
for (i = 1; i <= m; i++) if (index($0, "\"" T[i] "\"") && match($0, "\"" T[i] "\"[ \t]*:")) print "npm\t" dir "\t" T[i] "\t" file ":" NR "\t" T[i]
# Every "name": "github:owner/repo#tag" pair on the line, wherever it stands.
line = $0
while (match(line, /"[^"]+"[ \t]*:[ \t]*"github:[^"#]+#/)) {
pair = substr(line, RSTART, RLENGTH); line = substr(line, RSTART + RLENGTH)
name = pair; sub(/^"/, "", name); sub(/".*/, "", name)
s = pair; sub(/.*"github:/, "", s); sub(/#$/, "", s)
if (s in R) print "npm\t" dir "\t" name "\t" file ":" NR "\t" name
}
}' "$f"
done
find "$ROOT" -name .terraform -prune -o -name .git -prune -o -type f -name '*.tf' -print | while IFS= read -r f; do
awk -v fam="$family" -v file="${f#"$ROOT"/}" -v dir="$(dirof "$f")" '
BEGIN { n = split(fam, F, " "); for (i = 1; i <= n; i++) R[F[i]] = 1 }
/source[ \t]*=[ \t]*"git::https:\/\/github\.com\// {
s = $0; sub(/.*"git::/, "", s); sub(/[?"].*/, "", s)
r = s; sub(/^https:\/\/github\.com\//, "", r); split(r, p, "/"); r = p[1] "/" p[2]; sub(/\.git$/, "", r)
if (r in R) print "terraform\t" dir "\t" s "\t" file ":" NR "\t*" r "*"
}' "$f"
done
)
# A pin a block watches and no pattern of that block covers is a hit. Globbing is off while the
# patterns are split, so "*owner/repo*" stays a pattern and is not expanded against the tree.
set -f
bot_hits=$(printf '%s\n' "$pins" | while IFS="$tab" read -r eco dir name where suggest; do
[ -n "$eco" ] || continue
printf '%s\n' "$blocks" | grep -q "^$eco$tab$dir$tab" || continue
covered=no
for p in $(printf '%s\n' "$blocks" | awk -F'\t' -v e="$eco" -v d="$dir" '$1 == e && $2 == d && $3 != "" { print $3 }'); do
# shellcheck disable=SC2254 # the pattern is meant to match as a glob
case "$name" in ($p) covered=yes; break ;; esac
done
if [ "$covered" = no ]; then
echo "✗ .github/dependabot.yml: the $eco block for $dir watches $name ($where), a family pin the resync moves; add - dependency-name: \"$suggest\" to its ignore"
fi
done)
set +f
fi

if [ -n "$hits" ] || [ -n "$title_hit" ] || [ -n "$bot_hits" ]; then
if [ -n "$hits" ]; then echo "$hits"; fi
if [ -n "$title_hit" ]; then echo "$title_hit"; fi
if [ -n "$bot_hits" ]; then echo "$bot_hits"; fi
exit 1
fi
echo "✓ every Markdown file follows WRITING.md"
8 changes: 4 additions & 4 deletions conventions/manifest.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
{
"repo": "robertblust/conventions",
"tag": "v1.43.0",
"tag": "v1.44.0",
"files": {
"conventions/WRITING.md": "sha256:51d9a902ec3038392160df07f86aba98d34db1d4dbfaa5f4a647590eaccfb390",
"conventions/WORKING.md": "sha256:3a7a6b4ab1bd23f2407fc6ada2dd0512710a49d233fa7c3342951f6fc526ab23",
"conventions/REPOSITORIES.md": "sha256:461654ef06ee307d2e10daba201e97dbcad093cf6b3b7c35b6bad3627f9f4fec",
"conventions/PINS.md": "sha256:754a2f3d3f442e58a3ca05d6832c1062b92df80b21eb4d36e94c4b16fd302e10",
"conventions/PINS.md": "sha256:68df67ae820a8673d3130582aa829d4283176d3c8ec0a69adda3132a1a755b93",
"conventions/WRITER.md": "sha256:bc3b92683130b75993f824f0e34073d4344846c91be2009fd90c39099fd4fd48",
"conventions/TRANSLATOR.md": "sha256:3a3d2d0916d7230cbc760bced1be249b11cc516956fb41ad010b3216f23024ba",
"conventions/GLOSSARY.md": "sha256:1d0fbdb8415f8a65ccee75b4a26617d7aa1a4b146988cc2a2a8155fe67c02623",
"conventions/GERMAN.md": "sha256:2f4651dd74e2970f0751621bf9264441b43ee619b8a6e1373d5f3c7809581f77",
"conventions/EDITOR.md": "sha256:e65d0b2893ac841faefede9b873f7e33c1fd7b02bedf97d37d5362c0e83c08ea",
"conventions/BACKREADER.md": "sha256:36b12d4bee8e4fe190469e2f550d50d8a7f6dcab6bec573da17eb1c165c417cd",
"conventions/AGENTS.md": "sha256:9a6774f72436107eb94128f59a55b746fa9be987f8f6ac02d542283f32c0d03c",
"conventions/AGENTS.md": "sha256:2c71f0d0d0f6c2adfa2e343dc7b3565e84d8a8497bdbe52a6077d64a6e2bac00",
"conventions/conventions-sync": "sha256:a190ff79c2d8d3f6f6d9a597e9f261654f2419d30e2ac43abf6c64ebe2c47ed9",
"conventions/conventions-check": "sha256:30b2528e84381f04fd6b06b26ef103cd2360d877639706eb0b53f362a10fc433",
"conventions/conventions-check": "sha256:fb55ca4b4cfc5caa0d346ad6c705d4402125155f95db6c974099a8112e275c69",
"conventions/conventions-format": "sha256:2911a0d30dda1bc0f621522a9538b3e39704eee03458e65ba320eccbf39b5c7f",
"conventions/markdown-rules.cjs": "sha256:5cbf6120bf16e358b3a4e362015d054f0cd6e47ddfb0e5fe5ac17143b2bdef26",
"conventions/vscode-settings.json": "sha256:86d3c951f7c06d86ee3bc828ad6e49fac3531be98117a7a590cc75da7ba3600a",
Expand Down
Loading