Skip to content

release: v2.3.0 — Tier 1 agent feedback channel #11

release: v2.3.0 — Tier 1 agent feedback channel

release: v2.3.0 — Tier 1 agent feedback channel #11

Workflow file for this run

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 \
RELEASING.md
# 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: |
# REV-16 principle fix (2026-06-11): allowlist-based assertion replaces
# predicate-symmetric ABSENT checks. Catches under-strip AND any future
# scratch-pattern (BUGFIX_NOTES.md, DRAFT_v2.md, etc.) automatically,
# instead of requiring a workflow edit per new pattern. The check is
# orthogonal to the strip step's predicate — if the strip's pattern is
# wrong, this allowlist still fails-loud.
#
# MAINTENANCE CONTRACT: every legitimate new root file or directory must
# be added to EXPECTED_ROOT below. CI will fail-loud until it is.
# See 260607-w84-REVIEW.md REV-16 for the engineering rationale.
# printf '%s\n' (not heredoc) — heredoc closing EOF must sit at
# column 0, which violates YAML's run-block indentation rule and
# makes GitHub refuse to parse the workflow. printf+continuations
# are valid YAML *and* valid bash.
EXPECTED_ROOT=$(printf '%s\n' \
.git .claude .claude-plugin .github .gitignore .well-known \
apple-touch-icon.png CHANGELOG.md CLAUDE.md \
favicon-16x16.png favicon-32x32.png favicon.ico favicon.svg \
feedback index.html kleros-curate kleros-ipfs-upload LICENSE \
netlify netlify.toml openclaw-skill README.md robots.txt \
sitemap.xml SKILL.md | sort)
ACTUAL_ROOT=$(ls -A1 | sort)
UNEXPECTED=$(comm -23 <(echo "$ACTUAL_ROOT") <(echo "$EXPECTED_ROOT"))
if [ -n "$UNEXPECTED" ]; then
echo "FATAL: unexpected root entries after strip:"
echo "$UNEXPECTED"
echo ""
echo "If intentional: add the entry to EXPECTED_ROOT in sync-master.yml and re-run."
echo "If accidental: extend the strip-list or fix the source on dev."
exit 1
fi
# keep-list assertions — must be PRESENT (catches strip overreach)
for path in kleros-curate kleros-ipfs-upload feedback .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