Skip to content

feat: propose the pin move when an imported skill changes upstream - #27

Merged
xaleryb merged 8 commits into
intel:mainfrom
xaleryb:ci/upstream-sync
Sep 25, 2026
Merged

xaleryb merged 8 commits into
intel:mainfrom
xaleryb:ci/upstream-sync

Conversation

@xaleryb

@xaleryb xaleryb commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

sync_external.py --check proves an imported skill is still the bytes of its pinned commit. Nothing proved the pin is still the commit upstream maintains, and that failure is silent: every check here stays green while what this catalog publishes falls behind what its authors ship. This is the other half.

  • tools/check_upstream.py — compares the tree object id of each external-path at the pin against the same path at the tip of upstream's default branch. When a pinned directory moved, it moves external-commit everywhere this repository writes it down (every entry of that upstream, the twelve characters the group comment quotes, and NOTICE), re-vendors with sync_external.py --write, and opens one pull request per upstream on sync/<upstream>-<commit>.
  • tools/upstream_git.py — the transport, now shared with sync_external.py.
  • .github/workflows/upstream-sync.yml — Sundays and Wednesdays plus workflow_dispatch with a dry-run input; runs only in intel/skills; the one workflow here that writes.
  • validate.yml — the detector's self-test, beside the two self-tests already there, because a scheduled job that stops detecting is not noticed until its next run.

What it does not do is decide whether the new text should be published. An import is somebody else's document, so a change in it is a change somebody made there: the pull request's checks answer the structural half and a reviewer reads the diff.

Why a directory hash and not upstream's HEAD. Most commits upstream touch none of the skills imported here, so following HEAD would open a pull request that moves 23 pins, rewrites 23 .source.json files and changes no skill text. Per-path comparison costs 13.75 KiB of transfer per upstream and the whole survey runs in about three seconds; nothing is checked out, so the same code runs offline as the self-test above.

No third-party code receives the write token — plain git and gh, the stance security.yml already states.

Token. No secret is needed. A pull request opened with GITHUB_TOKEN gets its checks in GitHub's approval-required state, and a maintainer starts them with Approve workflows to run; every pull request this opens says so in its body. An app or account token in UPSTREAM_SYNC_TOKEN only removes that click. What it does need is the repository setting Allow GitHub Actions to create and approve pull requests: with it off, the run fails at gh pr create rather than quietly.

Run, not assumed

  • chore: move the intel/gpu-ai-skills pin to 0b4fafd09c5e #26 is this tool's own output, opened by it against this repository: intel/gpu-ai-skills moved 3fb51a0b6952 -> 0b4fafd09c5e, 9 of 20 pinned directories changed, 37 files, and the level 1 gate is green on it. intel/intel-performance-skills is at its pin and was correctly left alone.
  • --self-test: 21 assertions against this tree's real pins, offline. --mutate M1..M9 is nine distinct ways the detector can break — every one of them reports "nothing moved upstream", and every one of them turns the self-test red.
  • Idempotence on the live pull request: a second run answers SKIP intel/gpu-ai-skills: #26 (open) already proposes sync/gpu-ai-skills-0b4fafd09c5e, including the case where the head branch lives in a fork.
  • Failure contracts: an uncommitted working tree fails rather than carrying its changes into a pull request; a pin that no longer resolves fails; an upstream that cannot be reached warns and exits 0, for the same reason the link check does.
  • actionlint and zizmor --min-severity=medium --min-confidence=medium are clean.

Not run yet: the workflow itself has never executed on a runner — the schedule, the empty dry-run input on a schedule event, env.SYNC_TOKEN_SET and the repository guard are static-checked only. A workflow_dispatch with dry-run: true right after this merges is what closes that, and it changes nothing when it runs. Whether that repository setting is on here is something only the first real run can show.

Docs are in the same change: MAINTAINERS.md gains the workflow row, what the write permissions are for, the approval step and the one setting it needs, and how to run the same proposal by hand from a fork.

`sync_external.py --check` proves the vendored copy is still the pinned commit.
Nothing proved the pin is still the commit upstream maintains, and that failure
is silent: a stale pin keeps every check green while what this catalog publishes
falls behind what its authors ship.

`check_upstream.py` asks the other half. For each upstream it reads the tip of
the default branch upstream itself names through HEAD, then compares the tree
object id of every pinned directory at the pin against the same path at that
tip. Directory hashes rather than repository HEADs is what keeps it quiet: an
upstream commit that does not touch a pinned path is not an update here, and
reporting one would open a pull request that rewrites 23 `.source.json` files
and changes no skill text. It needs no file content, so two commits of
intel/gpu-ai-skills cost 13.75 KiB and the whole survey runs in 3 s.

When a directory has moved it moves the pin everywhere the pin is written down
- every entry of that upstream, the twelve characters the catalog comment
quotes, and NOTICE - re-vendors with `sync_external.py --write`, and opens one
pull request per upstream. The branch is named after the target commit, so a
second run finds its own pull request and stops. What the pull request does not
do is decide whether the new bytes should be published; that is what its checks
and its reviewer are for.

`upstream-sync.yml` runs it weekly in intel/skills only, or by hand with a
dry-run input. GitHub starts no workflow run for anything GITHUB_TOKEN does, so
with the fallback token the pull request arrives correct but unchecked: the run
warns and the body says so, and an UPSTREAM_SYNC_TOKEN secret fixes it.

Every way this detector can break leaves it reporting that nothing moved, which
is indistinguishable from an upstream nobody touched, so `--self-test` asserts
it against this tree's own catalog and `--mutate M1..M7` shows each break
turning it red. It runs on every pull request in validate.yml, not only on the
schedule, because a pull request that breaks it would otherwise be found out
about on a Monday.
`gh pr list` and `gh pr create` with no `--repo` resolve the repository from the
remotes of the checkout they run in. In CI that is one remote and the answer is
right. In a maintainer's clone it is several, and gh picks by its own rules: run
from a clone that has the fork and two other remotes, `gh pr list --state open`
answered about a repository nobody had named, and `--open-pr` would have opened
the pull request there.

So the repository comes from the remote the branch is pushed to, parsed from its
URL in both the https and the ssh spelling, and anything that is not a github
repository is refused rather than guessed. M8 is that refusal removed.

Found by running the whole propose() path against a throwaway bare remote with a
stand-in gh, which is also how the branch it pushes was checked: 37 files, and
the 9 skills the detector names are exactly the 9 whose files other than
.source.json changed.
The workflow pushes to the repository it runs in, so the branch and the pull request are refs of the same place. Run by hand it is not: the branch goes to a fork, and gh has to be told whose branch it is or it names one of this repository's. --against separates the two, branches off the target's default rather than the fork's, and M9 is the break.
One em dash reached a print. The body says why it avoids them -- both go through the console encoding of whoever runs the tool -- and the report had no reason to be different.
The assertion passed a string starting with /tmp, which is a hardcoded-temp-path finding whether or not anything is created -- nothing is here, it is a url that must be refused. Two urls now stand for that: a relative path and a git remote on another host.
… not none

GitHub now creates pull_request runs for a pull request opened or updated with
GITHUB_TOKEN in an approval-required state, started by a maintainer with
"Approve workflows to run". The workflow comment, the run annotation, the
generated pull request body and MAINTAINERS.md all still said such a pull
request carries no checks until someone pushes to it. The annotation drops
from a warning to a notice, since it now describes the intended setup.

MAINTAINERS.md also names the one setting the no-secret path does need:
Allow GitHub Actions to create and approve pull requests.
@vchamarthi

Copy link
Copy Markdown

Non-blocking: detection is per-directory, but bump() re-vendors every skill in the upstream group.
On #26, all 20 .source.json files were rewritten but only 9 of 20 skills changed. changed_skills.py saw 20, so skillspector ran 20 scans (run 35152850182 (https://github.com/intel/skills/actions/runs/35152850182)), 11 of them over identical bytes.

classify() already knows which dirs changed. Follow-up:
changed_skills.py could skip skills whose only diff is .source.json. The weekly cron still catches scanner-rule drift.

@xaleryb

xaleryb commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Non-blocking: detection is per-directory, but bump() re-vendors every skill in the upstream group. On #26, all 20 .source.json files were rewritten but only 9 of 20 skills changed. changed_skills.py saw 20, so skillspector ran 20 scans (run 35152850182 (https://github.com/intel/skills/actions/runs/35152850182)), 11 of them over identical bytes.

classify() already knows which dirs changed. Follow-up: changed_skills.py could skip skills whose only diff is .source.json. The weekly cron still catches scanner-rule drift.

Going to merge as is as working initial version. Working on another PR with some improvements for that changes checker functionality.

@xaleryb
xaleryb merged commit f2fdcce into intel:main Sep 25, 2026
7 checks passed
@xaleryb
xaleryb deleted the ci/upstream-sync branch September 25, 2026 02:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants