Skip to content

Commit b03e63e

Browse files
authored
fix: harden ci and sync workflow (#1)
* fix: harden ci and sync workflow * chore: remove stale gitignore entry for renamed skill workspace * fix: replace mapfile with portable read loop for macOS bash 3.2 compat * fix: use newline-delimited read instead of NUL for bash 3.2 compat * fix: guard empty arrays in bash 3.2 set -u and declare local loop vars
1 parent a37f0c8 commit b03e63e

4 files changed

Lines changed: 53 additions & 13 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66
paths:
77
- ".github/workflows/ci.yml"
88
- ".github/scripts/**"
9+
- ".gitignore"
910
- "README.md"
1011
- "CITATION.cff"
1112
- "SECURITY.md"
@@ -17,6 +18,7 @@ on:
1718
paths:
1819
- ".github/workflows/ci.yml"
1920
- ".github/scripts/**"
21+
- ".gitignore"
2022
- "README.md"
2123
- "CITATION.cff"
2224
- "SECURITY.md"

.gitignore

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@ ENV/
4949
.DS_Store
5050
.vscode/
5151

52-
# Local transcript benchmark workspace
53-
python-best-practices-workspace/
54-
# ^ this ignore entry is stale (skill was renamed). Kept to not break existing clones.
55-
5652
# Local agent/tool state
5753
.omo/
5854
.open-mem/

scripts/sync-payload.sh

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
77
MANIFEST="$ROOT/scripts/payload-manifest.json"
88
PAYLOAD_DIR="$ROOT/skills/python-project-workflow"
99
CI_MODE=false
10+
MANIFEST_FILES=()
11+
MANIFEST_SCRIPTS=()
1012

1113
[ "${1:-}" = "--ci" ] && CI_MODE=true
1214

@@ -17,6 +19,42 @@ DRIFT=false
1719
CHANGED=false
1820
SYNC_ERROR=false
1921

22+
manifest_entries() {
23+
local key="$1"
24+
25+
python3 - "$MANIFEST" "$key" <<'PY'
26+
import json
27+
import sys
28+
29+
path, key = sys.argv[1], sys.argv[2]
30+
with open(path, encoding="utf-8") as fh:
31+
data = json.load(fh)
32+
for item in data.get(key, []):
33+
print(item)
34+
PY
35+
}
36+
37+
manifest_scalar() {
38+
local key="$1"
39+
40+
python3 - "$MANIFEST" "$key" <<'PY'
41+
import json
42+
import sys
43+
44+
path, key = sys.argv[1], sys.argv[2]
45+
with open(path, encoding="utf-8") as fh:
46+
data = json.load(fh)
47+
value = data.get(key, "")
48+
sys.stdout.write(value if isinstance(value, str) else "")
49+
PY
50+
}
51+
52+
# Portable newline-delimited read (compatible with bash 3.2 on macOS).
53+
# The ;echo guarantees at least one line so arrays are never empty under set -u.
54+
while IFS= read -r item; do MANIFEST_FILES+=("$item"); done < <(manifest_entries files; echo)
55+
while IFS= read -r item; do MANIFEST_SCRIPTS+=("$item"); done < <(manifest_entries scripts; echo)
56+
REF_MODE="$(manifest_scalar references)"
57+
2058
mode_matches() {
2159
local target="$1"
2260
local mode="$2"
@@ -58,19 +96,20 @@ sync_file() {
5896

5997
is_covered() {
6098
local rel="$1"
99+
local f s
61100
# Authored-in-place files (written directly in payload, not copied)
62101
case "$rel" in
63102
SKILL.md) return 0 ;;
64103
esac
65-
for f in $(python3 -c "import json; d=json.load(open('$MANIFEST')); print('\n'.join(str(x) for x in d.get('files',[])))" 2>/dev/null); do
104+
for f in "${MANIFEST_FILES[@]}"; do
105+
[ -z "$f" ] && continue
66106
[ "$rel" = "$f" ] && return 0
67107
done
68-
for s in $(python3 -c "import json; d=json.load(open('$MANIFEST')); print('\n'.join(str(x) for x in d.get('scripts',[])))" 2>/dev/null); do
108+
for s in "${MANIFEST_SCRIPTS[@]}"; do
109+
[ -z "$s" ] && continue
69110
[ "$rel" = "scripts/$s" ] && return 0
70111
done
71-
local ref_mode
72-
ref_mode=$(python3 -c "import json; d=json.load(open('$MANIFEST')); r=d.get('references',''); print(r if isinstance(r,str) else '')" 2>/dev/null)
73-
if [ "$ref_mode" = "*" ]; then
112+
if [ "$REF_MODE" = "*" ]; then
74113
case "$rel" in
75114
references/*) [ -f "$ROOT/$rel" ] && return 0 ;;
76115
esac
@@ -79,14 +118,16 @@ is_covered() {
79118
}
80119

81120
# Files
82-
for f in $(python3 -c "import json; d=json.load(open('$MANIFEST')); print('\n'.join(str(x) for x in d.get('files',[])))" 2>/dev/null); do
121+
for f in "${MANIFEST_FILES[@]}"; do
122+
[ -z "$f" ] && continue
83123
source="$ROOT/$f"
84124
target="$PAYLOAD_DIR/$f"
85125
sync_file "$source" "$target" "$f"
86126
done
87127

88128
# Scripts
89-
for s in $(python3 -c "import json; d=json.load(open('$MANIFEST')); print('\n'.join(str(x) for x in d.get('scripts',[])))" 2>/dev/null); do
129+
for s in "${MANIFEST_SCRIPTS[@]}"; do
130+
[ -z "$s" ] && continue
90131
source="$ROOT/scripts/$s"
91132
target="$PAYLOAD_DIR/scripts/$s"
92133
mode=644
@@ -95,8 +136,7 @@ for s in $(python3 -c "import json; d=json.load(open('$MANIFEST')); print('\n'.j
95136
done
96137

97138
# References (mirror)
98-
ref_mode=$(python3 -c "import json; d=json.load(open('$MANIFEST')); r=d.get('references',''); print(r if isinstance(r,str) else '')" 2>/dev/null)
99-
if [ "$ref_mode" = "*" ]; then
139+
if [ "$REF_MODE" = "*" ]; then
100140
for source in "$ROOT/references/"*.md; do
101141
if [ ! -f "$source" ]; then
102142
echo " MISSING source: references/*.md"

scripts/validate-ci.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ def main() -> int:
2929
if validate is None:
3030
errors.append("ci.yml: missing validate job")
3131
else:
32+
if '".gitignore"' not in workflow:
33+
errors.append("ci.yml: validation workflow must trigger on .gitignore changes")
3234
if "python3 scripts/verify-urls.py" in validate:
3335
errors.append(
3436
"ci.yml: live URL checks must not run in the validation matrix"

0 commit comments

Comments
 (0)