-
Notifications
You must be signed in to change notification settings - Fork 0
219 lines (203 loc) · 10.9 KB
/
Copy pathsync-master.yml
File metadata and controls
219 lines (203 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
name: Sync master from dev
on:
push:
tags:
- "*@v*.*.*" # skill release tags: kleros-curate@v1.0.0, kleros-ipfs-upload@v1.1.0
- "v[0-9]*" # repo-level/whole-plugin releases: v1.2.0 (REV-05: digit-anchored to skip vNext, vendor-*, etc.)
workflow_dispatch: # manual override for hotfixes / initial dry-run (gated by production-sync Environment)
# Principle of least privilege for the implicit GITHUB_TOKEN.
# The actual master push uses the GitHub App token minted below — this block
# only restricts what the workflow's auto-injected GITHUB_TOKEN can do.
permissions:
contents: read
# Never drop a running sync — queue the next one instead.
concurrency:
group: sync-master
cancel-in-progress: false
jobs:
sync:
runs-on: ubuntu-latest
timeout-minutes: 15 # hard cap; default is 6h — refuse to silently burn budget
# REV-07: gate the job on a protected Environment.
# Configure `production-sync` in repo Settings → Environments with:
# - Required reviewers: org owners only (or named maintainers)
# - Deployment branches: restrict to `dev` (defense in depth)
# workflow_dispatch then requires reviewer approval before the App token
# is minted, closing the privilege-amplification vector for non-tag runs.
environment: production-sync
steps:
# -----------------------------------------------------------------------
# 1. Mint a short-lived GitHub App installation token (auto-revoked at job end).
# App must be registered in the kleros org, scoped to this repo only,
# with Contents: Read and write. No other permissions needed.
#
# SHA-pinned (REV-02): this Action sees GH_APP_PRIVATE_KEY as input.
# A mutable @v2 tag would let a compromised release exfiltrate the PEM.
# Update the pin alongside any deliberate version bump.
# -----------------------------------------------------------------------
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
# client-id (not app-id) per action v3.1.0+ deprecation.
# Inlined because client-id is a non-sensitive identifier (visible
# on the App's public page); only the private key is a real secret.
client-id: Iv23liKuhygE4qmFei7w
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
# -----------------------------------------------------------------------
# 2. Check out dev (shallow — full history not needed for strip+push).
# -----------------------------------------------------------------------
- name: Checkout dev
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v6.0.3
with:
ref: dev
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 1
# Fetch origin/master so --force-with-lease has a known remote-tracking ref.
# The "|| true" handles the edge case where master does not yet exist on origin
# (e.g. very first bootstrap run before any master commits).
- name: Fetch origin/master for force-with-lease safety
run: git fetch origin master:refs/remotes/origin/master || true
# -----------------------------------------------------------------------
# REV-03: Tag ancestry check.
# Verify the triggering commit is reachable from origin/dev before any
# destructive operation. Closes the privilege-amplification vector where
# a contributor pushes a tag at an arbitrary (e.g. malicious) commit.
#
# github.sha is a 40-char hex hash from GitHub itself (not user-controllable
# text), so direct interpolation in `run:` is safe. The env-var pattern is
# used anyway as defensive coding consistent with the workflow's other steps.
#
# Skipped for workflow_dispatch — that path is gated by the Environment
# required-reviewers check (REV-07).
# -----------------------------------------------------------------------
- name: Verify trigger commit is on dev ancestry
if: github.event_name == 'push'
env:
TRIGGER_SHA: ${{ github.sha }}
TRIGGER_REF: ${{ github.ref_name }}
run: |
# Fetch enough dev history for the merge-base lookup.
# 500 commits is generous; the existing checkout was --depth=1.
git fetch origin dev --depth=500
if ! git merge-base --is-ancestor "$TRIGGER_SHA" origin/dev; then
echo "ERROR: tag $TRIGGER_REF (commit $TRIGGER_SHA) is not reachable"
echo " from origin/dev. Refusing to sync master — release tags"
echo " must point to commits on dev's history."
exit 1
fi
echo "OK: $TRIGGER_REF ($TRIGGER_SHA) is on dev ancestry."
# -----------------------------------------------------------------------
# 3. Set up Node for the quality gates.
# -----------------------------------------------------------------------
- name: Set up Node 24
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v6.4.0
with:
node-version: "24"
# -----------------------------------------------------------------------
# 4. Quality gate — test suite must pass before we touch master.
# -----------------------------------------------------------------------
- name: Quality gate — npm test
run: npm test
# -----------------------------------------------------------------------
# 5. Quality gate — index.json digests must be fresh.
# Re-running update-digests and checking for a diff ensures no stale
# SHA-256 hashes ship to user machines.
# -----------------------------------------------------------------------
- name: Quality gate — digest freshness
run: |
node scripts/update-digests.js
if ! git diff --quiet .well-known/agent-skills/index.json; then
echo "ERROR: .well-known/agent-skills/index.json digests are stale."
echo "Run 'npm run update-digests' locally and commit the result before tagging."
exit 1
fi
# -----------------------------------------------------------------------
# 6. Strip dev-only files.
# Everything removed here will NOT appear on master or in plugin installs.
#
# Strip-list:
# .planning/ — GSD planning artefacts
# test/ — test suite
# scripts/ — dev helper scripts
# package.json — dev dependencies / npm scripts
# yarn.lock — lockfile
# .yarnrc.yml — Yarn config
# *FEEDBACK*.md — AI session feedback scratch files (root only)
# HANDOVER*.md — Handover scratch files (root only)
# -----------------------------------------------------------------------
- name: Strip dev-only files
run: |
git rm -r --quiet --ignore-unmatch \
.planning \
test \
scripts \
package.json \
yarn.lock \
.yarnrc.yml
# Remove root feedback/handover md files by pattern.
# REV-01: root-anchored with [^/]* — previous '^.*_FEEDBACK.*\.md$' over-matched
# paths at ANY depth (e.g. kleros-curate/MIGRATION_FEEDBACK.md), which would
# silently delete legitimate skill-dir docs from user installs.
# 2026-06-11 fix: dropped the leading underscore requirement — earlier
# '_FEEDBACK' pattern missed files like FEEDBACK_FROM_CLAUDE_MARKETPLACE.md
# where FEEDBACK is at position 0. Sanity check below was also broadened.
git ls-files -z | \
grep -zE '^[^/]*FEEDBACK[^/]*\.md$|^HANDOVER[^/]*\.md$' | \
xargs -0 --no-run-if-empty git rm --quiet
echo "Strip complete. Remaining tracked files:"
git ls-files | head -60
# -----------------------------------------------------------------------
# REV-09: Sanity check the stripped tree before committing.
# Catches both over-strip (keep-list path disappeared) and under-strip
# (strip-list path survived). Cheap insurance against future strip-list
# edits that go wrong.
# -----------------------------------------------------------------------
- name: Sanity check stripped tree
run: |
# strip-list assertions — must be ABSENT
for path in .planning test scripts package.json yarn.lock .yarnrc.yml; do
if [ -e "$path" ]; then
echo "FATAL: strip-list path '$path' still present after strip"
exit 1
fi
done
# Root feedback/handover files must be ABSENT
if compgen -G "*FEEDBACK*.md" > /dev/null 2>&1 || \
compgen -G "HANDOVER*.md" > /dev/null 2>&1; then
echo "FATAL: root *FEEDBACK*.md or HANDOVER*.md still present"
exit 1
fi
# keep-list assertions — must be PRESENT (catches strip overreach)
for path in kleros-curate kleros-ipfs-upload .well-known/agent-skills \
.github/workflows SKILL.md index.html README.md LICENSE \
CHANGELOG.md robots.txt sitemap.xml; do
if [ ! -e "$path" ]; then
echo "FATAL: keep-list path '$path' missing after strip — overreach"
exit 1
fi
done
echo "OK: stripped tree looks correct."
# -----------------------------------------------------------------------
# 7. Configure git identity for the bot commit on master.
# REV-11: identity derived dynamically from the App token output.
# Whatever name the user registered the App with (e.g. kleros-skills-bot
# or kleros-sync), the commit author will match the actual App slug —
# no hardcoded string to drift out of sync.
# -----------------------------------------------------------------------
- name: Configure git identity
env:
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
git config user.name "${APP_SLUG}[bot]"
git config user.email "${APP_SLUG}[bot]@users.noreply.github.com"
# -----------------------------------------------------------------------
# 8. Commit the stripped tree and push to master.
# --force-with-lease rejects the push if someone manually pushed to
# master between our checkout and this step (safer than --force).
# The commit is created even when deletions are the only changes.
# -----------------------------------------------------------------------
- name: Commit stripped tree and push to master
run: |
git commit -m "chore(master): sync from dev (strip dev-only files)"
git push origin HEAD:master --force-with-lease