Repository navigation
147 lines (141 loc) · 6.96 KB
/
Copy pathdevkit-sync.yml
File metadata and controls
147 lines (141 loc) · 6.96 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
# Reusable devkit-sync workflow (SPEC DEC-044).
#
# Ported from the skills-sync mechanism, but devkit-scoped: it keeps a consumer
# plugin repo's vendored devkit submodule (tests/_testkit →
# agent-zero-plugin-development-testkit) current by bumping it to the devkit's
# latest main and opening an auto-merge PR. No `.skills`, no asset re-linking —
# the submodule IS the content.
#
# A consumer invokes it via a thin caller (templates/devkit-sync.caller.yml,
# copied in by `make link-workflows`):
#
# jobs:
# sync:
# uses: agent-zero-plugins/agent-zero-plugin-development-testkit/.github/workflows/devkit-sync.yml@main
# with: { devkit-submodule-path: "tests/_testkit" }
# secrets:
# SKILLS_SYNC_APP_CLIENT_ID: ${{ secrets.SKILLS_SYNC_APP_CLIENT_ID }}
# SKILLS_SYNC_APP_PRIVATE_KEY: ${{ secrets.SKILLS_SYNC_APP_PRIVATE_KEY }}
#
# Auth model (same as skills-sync): an App-minted token (Contents:Read on the
# devkit repo) is used ONLY for the cross-repo submodule fetch; the consumer's
# GITHUB_TOKEN does the branch push + PR + auto-merge. GITHUB_TOKEN cannot write
# .github/workflows/, so this workflow never touches workflow files — only the
# submodule pin.
name: devkit-sync
on:
workflow_call:
inputs:
devkit-submodule-path:
description: "Path to the vendored devkit submodule in the consumer."
required: false
type: string
default: "tests/_testkit"
branch-name:
description: "Branch name for the sync PR."
required: false
type: string
default: "chore/devkit-sync"
auto-merge:
description: "Enable auto-merge on the PR (requires branch protection)."
required: false
type: boolean
default: true
secrets:
SKILLS_SYNC_APP_CLIENT_ID:
description: "OAuth Client ID (Iv23li…) of the sync GitHub App."
required: true
SKILLS_SYNC_APP_PRIVATE_KEY:
description: "PEM of the sync GitHub App's private key."
required: true
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Mint App installation token (read-only — devkit submodule clone)
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.SKILLS_SYNC_APP_CLIENT_ID }}
private-key: ${{ secrets.SKILLS_SYNC_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Checkout consumer repo (GITHUB_TOKEN, no submodules)
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: false
fetch-depth: 0
- name: Bump the devkit submodule to the latest release tag (App token)
id: bump
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
DEVKIT_PATH: ${{ inputs.devkit-submodule-path }}
run: |
set -euo pipefail
AUTH=$(printf 'x-access-token:%s' "$APP_TOKEN" | base64 -w0)
GIT_AUTH=(-c "http.https://github.com/.extraheader=AUTHORIZATION: basic $AUTH")
git "${GIT_AUTH[@]}" submodule update --init "$DEVKIT_PATH"
before=$(git -C "$DEVKIT_PATH" rev-parse HEAD)
# Track the latest release tag WITHIN A MAJOR CHANNEL — auto-updates get
# non-breaking releases; a MAJOR (e.g. v2, which hard-requires BDD) is
# opt-in per repo (bump deliberately via `make update-devkit DEVKIT_REF=v2.0.0`).
# Channel = .devkit.yml `devkit_major` (default 1) so nobody is force-jumped.
MAJOR=$(sed -nE 's/^devkit_major:[[:space:]]*"?([0-9]+)"?.*/\1/p' .devkit.yml 2>/dev/null | head -1)
MAJOR="${MAJOR:-1}"
git -C "$DEVKIT_PATH" "${GIT_AUTH[@]}" fetch --tags --force origin
ref=$(git -C "$DEVKIT_PATH" tag -l "v$MAJOR.*" --sort=-v:refname | head -1)
if [ -n "$ref" ]; then
echo "target=$ref (channel v$MAJOR)" >> "$GITHUB_OUTPUT"
git -C "$DEVKIT_PATH" checkout --detach "refs/tags/$ref"
else
echo "target=main (no v$MAJOR.* tags yet)" >> "$GITHUB_OUTPUT"
git -C "$DEVKIT_PATH" "${GIT_AUTH[@]}" fetch origin main
git -C "$DEVKIT_PATH" checkout --detach FETCH_HEAD
fi
after=$(git -C "$DEVKIT_PATH" rev-parse HEAD)
if [ "$before" = "$after" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "devkit already at ${ref:-main} ($after) — nothing to sync."
else
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "devkit $before → $after (${ref:-main})"
git add "$DEVKIT_PATH"
fi
- name: Commit, push, PR, auto-merge (GITHUB_TOKEN)
if: steps.bump.outputs.changed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH_NAME: ${{ inputs.branch-name }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
AUTO_MERGE: ${{ inputs.auto-merge }}
DEVKIT_PATH: ${{ inputs.devkit-submodule-path }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# NO `git push origin --delete "$BRANCH_NAME"` here (DEC-079).
#
# Deleting the head ref DELETES THE PR WITH IT: GitHub auto-closes any
# open PR whose head branch disappears. That is what closed 18 sync PRs
# across the fleet, including four that were already fully green — the
# e2e had passed, and the work was discarded and re-queued for nothing.
# It also made the `gh pr edit` reuse path below dead code: the delete
# closed the very PR the next line goes looking for, so `existing` was
# always empty and every run created a brand-new PR.
#
# Force-pushing the branch is sufficient and non-destructive: an open PR
# follows its head ref and simply picks up the new commit.
git checkout -B "$BRANCH_NAME"
git commit -m "chore: sync devkit submodule to ${{ steps.bump.outputs.target }}"
git push --force origin "$BRANCH_NAME"
body="Automated devkit sync — bumped \`$DEVKIT_PATH\` to the devkit's latest release (\`${{ steps.bump.outputs.target }}\`)."
existing=$(gh pr list --head "$BRANCH_NAME" --state open --json number --jq '.[0].number // empty' 2>/dev/null || true)
if [ -n "$existing" ]; then
gh pr edit "$existing" --title "chore: sync devkit submodule" --body "$body"
pr="$existing"
else
gh pr create --title "chore: sync devkit submodule" --body "$body" --head "$BRANCH_NAME" --base "$DEFAULT_BRANCH"
pr=$(gh pr list --head "$BRANCH_NAME" --state open --json number --jq '.[0].number')
fi
if [ "$AUTO_MERGE" = "true" ] && [ -n "$pr" ]; then
gh pr merge "$pr" --auto --squash || echo "::warning::auto-merge not enabled (branch protection?)."
fi