-
Notifications
You must be signed in to change notification settings - Fork 8
Sync skills to distribution repo on release #172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
bc32a03
Sync skills to basecamp/skills on release
jeremy 5d95ff4
Address review feedback on sync-skills
jeremy faf472e
Handle missing manifest on first sync run
jeremy ebcb8f4
Add job timeout and redact token from error output
jeremy b2dbaa2
Document bootstrap cleanup policy
jeremy 639d446
Pass SKILLS_TOKEN to subprocess in sync-skills-remote target
jeremy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| # Sync skills from basecamp-cli to basecamp/skills distribution repo. | ||
| # | ||
| # Env vars: | ||
| # SKILLS_TOKEN - GitHub token with push access to basecamp/skills (required unless DRY_RUN=local) | ||
| # RELEASE_TAG - Release tag, e.g. v1.2.3 (required) | ||
| # SOURCE_SHA - Source commit SHA (required) | ||
| # DRY_RUN - Optional: "local" (no network) or "remote" (clone but skip push) | ||
|
|
||
| RELEASE_TAG="${RELEASE_TAG:?RELEASE_TAG is required}" | ||
| SOURCE_SHA="${SOURCE_SHA:?SOURCE_SHA is required}" | ||
| DRY_RUN="${DRY_RUN:-}" | ||
|
|
||
| SKILLS_SOURCE="skills" | ||
| TARGET_REPO="basecamp/skills" | ||
| TARGET_BRANCH="main" | ||
|
|
||
| # --- Helpers --- | ||
|
|
||
| die() { echo "ERROR: $*" >&2; exit 1; } | ||
|
|
||
| assert_remote_url() { | ||
| local url | ||
| url=$(git -C "$1" remote get-url origin) | ||
| local stripped | ||
| stripped=$(echo "$url" | sed -E 's/\.git$//') | ||
| # Validate host + owner/repo for both HTTPS and SSH forms | ||
| case "$stripped" in | ||
| https://github.com/"$TARGET_REPO") ;; | ||
| https://x-access-token:*@github.com/"$TARGET_REPO") ;; | ||
| git@github.com:"$TARGET_REPO") ;; | ||
| *) die "origin remote '$(echo "$url" | sed -E 's#(https://[^:@]+:)[^@]*@#\1***@#')' does not point to github.com/$TARGET_REPO" ;; | ||
| esac | ||
| } | ||
|
|
||
| assert_branch() { | ||
| local branch | ||
| branch=$(git -C "$1" rev-parse --abbrev-ref HEAD) | ||
| [[ "$branch" == "$TARGET_BRANCH" ]] || die "checked-out branch is '$branch', expected '$TARGET_BRANCH'" | ||
| } | ||
|
|
||
| # --- Discover skills --- | ||
|
|
||
| skill_dirs=() | ||
| for skill_md in "$SKILLS_SOURCE"/*/SKILL.md; do | ||
| [[ -f "$skill_md" ]] || continue | ||
| skill_dirs+=("$(dirname "$skill_md")") | ||
| done | ||
|
|
||
| [[ ${#skill_dirs[@]} -gt 0 ]] || die "no skills found under $SKILLS_SOURCE/*/SKILL.md" | ||
| echo "Found ${#skill_dirs[@]} skill(s): ${skill_dirs[*]}" | ||
|
|
||
| # --- Copy skills into target, excluding *.go and dotfiles --- | ||
|
|
||
| copy_skills() { | ||
| local target_dir="$1" | ||
| for skill_dir in "${skill_dirs[@]}"; do | ||
| local name | ||
| name=$(basename "$skill_dir") | ||
| rm -rf "${target_dir:?}/${name}" | ||
| mkdir -p "$target_dir/$name" | ||
| # Copy files, excluding *.go and dotfiles | ||
| find "$skill_dir" -mindepth 1 \ | ||
| ! -name '*.go' \ | ||
| ! -name '.*' \ | ||
| ! -path '*/.*' \ | ||
| -type f \ | ||
| -exec bash -c ' | ||
| src="$1"; skill_dir="$2"; target_dir="$3" | ||
| rel="${src#$skill_dir/}" | ||
| mkdir -p "$(dirname "$target_dir/$rel")" | ||
| cp "$src" "$target_dir/$rel" | ||
| ' _ {} "$skill_dir" "$target_dir/$name" \; | ||
| done | ||
| } | ||
|
|
||
| # --- DRY_RUN=local: copy into tmpdir, diff against empty baseline --- | ||
|
|
||
| if [[ "$DRY_RUN" == "local" ]]; then | ||
| tmpdir=$(mktemp -d) | ||
| trap 'rm -rf "$tmpdir"' EXIT | ||
| echo "DRY_RUN=local: copying skills into $tmpdir" | ||
| copy_skills "$tmpdir" | ||
| echo "" | ||
| echo "=== Skills copied ===" | ||
| find "$tmpdir" -type f | sort | while read -r f; do | ||
| echo " ${f#$tmpdir/}" | ||
| done | ||
| echo "" | ||
| echo "=== Diff (against empty baseline) ===" | ||
| # Initialize as empty git repo to get a clean diff | ||
| git -C "$tmpdir" init -q | ||
| git -C "$tmpdir" add -A | ||
| git -C "$tmpdir" diff --cached --stat | ||
| echo "" | ||
| echo "DRY_RUN=local complete. No network operations performed." | ||
| exit 0 | ||
| fi | ||
|
|
||
| # --- Clone target repo --- | ||
|
|
||
| [[ -n "${SKILLS_TOKEN:-}" ]] || die "SKILLS_TOKEN is required (set DRY_RUN=local for offline testing)" | ||
|
|
||
| tmpdir=$(mktemp -d) | ||
| trap 'rm -rf "$tmpdir"' EXIT | ||
|
|
||
| echo "Cloning $TARGET_REPO into $tmpdir/skills..." | ||
| git clone --depth 1 --branch "$TARGET_BRANCH" \ | ||
| "https://x-access-token:${SKILLS_TOKEN}@github.com/${TARGET_REPO}.git" \ | ||
| "$tmpdir/skills" | ||
|
|
||
| target="$tmpdir/skills" | ||
|
|
||
| # --- Safety checks --- | ||
|
|
||
| assert_remote_url "$target" | ||
| assert_branch "$target" | ||
|
|
||
| # --- Copy skills --- | ||
|
|
||
| echo "Copying skills into target..." | ||
| copy_skills "$target" | ||
|
|
||
| # --- Remove stale skills using manifest --- | ||
| # .managed-skills lists skill names owned by this sync. Only those are | ||
| # candidates for deletion — other content in the target repo is untouched. | ||
|
|
||
| MANIFEST=".managed-skills" | ||
| source_skill_names=() | ||
| for skill_dir in "${skill_dirs[@]}"; do | ||
| source_skill_names+=("$(basename "$skill_dir")") | ||
| done | ||
|
|
||
| previously_managed_names=() | ||
| if [[ -f "$target/$MANIFEST" ]]; then | ||
| while IFS= read -r entry; do | ||
| [[ -z "$entry" ]] && continue | ||
| if [[ "$entry" == "." || "$entry" == ".." || ! "$entry" =~ ^[a-zA-Z0-9._-]+$ ]]; then | ||
| echo "WARNING: skipping invalid manifest entry: $entry" >&2 | ||
| continue | ||
| fi | ||
| previously_managed_names+=("$entry") | ||
| done < "$target/$MANIFEST" | ||
| else | ||
| # No manifest yet (first run). basecamp-cli is the source of truth, so | ||
| # treat all */SKILL.md dirs in the target as managed. Any skill not in the | ||
| # source set will be removed. Use DRY_RUN=remote to preview before pushing. | ||
| for candidate in "$target"/*/SKILL.md; do | ||
| [[ -f "$candidate" ]] || continue | ||
| previously_managed_names+=("$(basename "$(dirname "$candidate")")") | ||
| done | ||
| fi | ||
|
|
||
| for previously_managed in "${previously_managed_names[@]}"; do | ||
| found=0 | ||
| for name in "${source_skill_names[@]}"; do | ||
| [[ "$name" == "$previously_managed" ]] && found=1 && break | ||
| done | ||
| if [[ "$found" -eq 0 && -d "$target/$previously_managed" ]]; then | ||
| echo "Removing stale skill: $previously_managed" | ||
| rm -rf "${target:?}/$previously_managed" | ||
| fi | ||
| done | ||
|
|
||
| # Write current manifest | ||
| printf '%s\n' "${source_skill_names[@]}" | sort > "$target/$MANIFEST" | ||
|
|
||
| # --- Commit --- | ||
|
|
||
| git -C "$target" add -A | ||
|
|
||
| if git -C "$target" diff --cached --quiet; then | ||
| echo "No changes to commit. Skills are already up to date." | ||
| exit 0 | ||
| fi | ||
|
|
||
| echo "" | ||
| echo "=== Changes ===" | ||
| git -C "$target" diff --cached --stat | ||
| echo "" | ||
|
|
||
| if [[ "$DRY_RUN" == "remote" ]]; then | ||
| echo "DRY_RUN=remote: skipping commit and push." | ||
| echo "" | ||
| echo "=== Full diff ===" | ||
| git -C "$target" diff --cached | ||
| exit 0 | ||
| fi | ||
|
|
||
| git -C "$target" \ | ||
| -c user.name="basecamp-cli[bot]" \ | ||
| -c user.email="basecamp-cli[bot]@users.noreply.github.com" \ | ||
| commit -m "$(cat <<EOF | ||
| Sync skills from basecamp-cli ${RELEASE_TAG} | ||
|
|
||
| Source: basecamp/basecamp-cli@${SOURCE_SHA} | ||
| EOF | ||
| )" | ||
|
|
||
| # --- Push (with one retry on non-fast-forward) --- | ||
|
|
||
| push_target() { | ||
| git -C "$target" push origin "$TARGET_BRANCH" 2>&1 | ||
| } | ||
|
|
||
| if ! output=$(push_target); then | ||
| if echo "$output" | grep -qi "non-fast-forward"; then | ||
| echo "Push rejected (non-fast-forward). Pulling with rebase and retrying..." | ||
| git -C "$target" pull --rebase origin "$TARGET_BRANCH" | ||
| if ! retry_output=$(push_target); then | ||
| echo "$retry_output" >&2 | ||
| die "Push failed after retry" | ||
| fi | ||
| else | ||
| echo "$output" >&2 | ||
| die "Push failed" | ||
| fi | ||
| fi | ||
|
|
||
| echo "" | ||
| echo "Skills synced to $TARGET_REPO ($TARGET_BRANCH) from $RELEASE_TAG" |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.