Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ jobs:
- name: 馃И Self-test marketplace workflow authorization branches
run: bash scripts/marketplace-publication-workflow.test.sh

- name: 馃И Self-test marketplace publication checkout identity
run: bash scripts/marketplace-publication-identity.test.sh

- name: 馃И Self-test create-only marketplace proposals
run: bash scripts/propose-marketplace-release.test.sh

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-marketplace-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
RELEASE_COMMIT: ${{ github.sha }}
CI_RUN: ${{ inputs.ci-run || 'latest' }}
run: |
RELEASE_COMMIT=$(git rev-parse --verify HEAD)
bash scripts/prepare-merged-marketplace-release.sh \
--repo "$REPOSITORY" --release "$RELEASE_COMMIT" --ci-run "$CI_RUN" \
--output "$RUNNER_TEMP/marketplace-candidate" > "$RUNNER_TEMP/assessment.json"
Expand Down
8 changes: 7 additions & 1 deletion docs/marketplace-releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ this preparation workflow has no publishing job.

**Publish marketplace release** supports manual main dispatch and an opt-in hourly check at minute
25 UTC. It uses
current-main tooling and regenerates the candidate from the version proposal's sole parent. Both
current-main tooling and regenerates the candidate from the version proposal's sole parent. The
assessment commit and its output bind to the actual current-main checkout, including when main
advances after dispatch. Both
remote main and the named CI run are checked before and after verification. A stale, foreign,
failed, PR or unrelated workflow run is refused. An ordinary commit reports `NO_VERSION_CHANGE`
and cannot publish. No downloaded artifact supplies publication authority.
Expand Down Expand Up @@ -219,6 +221,8 @@ reuses the complete candidate reproduction, source parent, two-manifest tree and
in a disposable local repository. Only that private repository's copy of the candidate tag is
removed for reconstruction. The caller's tags, branches, index and working files are preserved;
neither GitHub nor another remote is contacted. Inherited Git layout overrides are neutralized.
Physical checkout pathnames retain all their bytes, including trailing newlines. Verification keeps
configured filesystem observation hooks inert and preserves the caller's index and configuration.

Success emits `status: INSPECTED`, `scope: local-historical-content`, and a `localTag` snapshot.
`PRESENT` records the tag object, resolved commit and whether it targets the nominated release;
Expand Down Expand Up @@ -302,6 +306,8 @@ or an open PR touching either marketplace manifest. A complete unrelated PR is n
latest selector refuses a pending or failed latest CI run rather than finding an older green one.
For renamed PR files, complete REST filename/status records must match GraphQL before the original
paths are considered. Moving a manifest away is a conflict; complete unrelated renames are allowed.
The CI workflow identity accepts its plain path or the exact main-qualified forms
`.github/workflows/ci.yaml@main` and `.github/workflows/ci.yaml@refs/heads/main`.
Two agreeing observations and byte-exact private reconstruction are required. `NO_CHANGE` performs
no writes, including when proposal creation was requested. Evidence artifacts supply no write authority.
Read-only candidate-release visibility is the reader's projection; it does not establish visibility of
Expand Down
11 changes: 7 additions & 4 deletions scripts/inspect-marketplace-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE GIT_OBJECT_DIRECTORY \
GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_PREFIX GIT_NAMESPACE GIT_CONFIG_PARAMETERS GIT_CONFIG_COUNT
unset GIT_CONFIG GIT_CONFIG_GLOBAL GIT_CONFIG_SYSTEM GIT_CONFIG_NOSYSTEM
export GIT_NO_REPLACE_OBJECTS=1 GIT_NO_LAZY_FETCH=1
original=$(git rev-parse --show-toplevel) || fail 'a Git working tree is required'
original=$(cd "$original" && pwd -P)
original=$(git rev-parse --show-toplevel && printf '.') || fail 'a Git working tree is required'
original=${original%$'\n.'}
original=$(cd "$original" && pwd -P && printf '.') || fail 'physical working tree is unavailable'
original=${original%$'\n.'}
temp=$(mktemp -d "${TMPDIR:-/tmp}/marketplace-inspect.XXXXXX")
trap 'rm -rf "$temp"' EXIT
# Check the original before cloning; a local clone need not retain these restrictions.
Expand Down Expand Up @@ -48,9 +50,10 @@ if [ -n "$tag_oid" ]; then
else
printf '%s\n' '{"state":"ABSENT","objectOid":null,"commitOid":null,"targetsRelease":null}' > "$temp/local-tag.json"
fi
# A separate local repository shares immutable objects, never the caller's refs or index.
# A separate local repository copies immutable objects, never the caller's refs or index.
# No checkout, network call or nominated source code is executed.
git clone --shared --no-checkout --quiet "$original" "$temp/repository"
# Shared-clone alternates use line records and cannot retain every physical pathname.
git clone --no-hardlinks --no-checkout --quiet "$original" "$temp/repository"
private=$(cd "$temp/repository" && pwd -P)
[ "$(git -C "$private" rev-parse --show-toplevel)" = "$private" ] || fail 'private worktree layout is invalid'
[ "$(git -C "$private" rev-parse --path-format=absolute --git-common-dir)" = "$private/.git" ] || fail 'private Git directory is invalid'
Expand Down
10 changes: 10 additions & 0 deletions scripts/inspect-marketplace-release.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ run > "$work/default"
jq -e '.status=="VERIFIED" and .scope=="local-prepublication"' "$work/default" >/dev/null
passed=$((passed+1))
accept 'absent tag is an inspection, never prepublication clearance' ABSENT null
fixture
newline_repo="$repo"$'\n'
mv "$repo" "$newline_repo"; repo=$newline_repo
snapshot "$work/newline-before"
accept 'exact trailing-newline checkout identity' ABSENT null
snapshot "$work/newline-after"
for field in refs status index; do
cmp "$work/newline-before.$field" "$work/newline-after.$field" || fail "newline checkout $field changed"
done
passed=$((passed+1))
git -C "$repo" tag v1.3.0 "$release"
if run > "$work/default" 2> "$work/error"; then fail 'default mode accepted occupied tag'; fi
[ ! -s "$work/default" ] || fail 'default emitted clearance'
Expand Down
4 changes: 3 additions & 1 deletion scripts/marketplace-observation-completeness.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ if { [ "${1:-}" = -C ] && [ "${2:-}" = "$OBS_REPO" ] && [ "${3:-}" = config ]; }
esac
fi
if [ "$OBS_FAULT" = graft ] && [ "${1:-}" = rev-parse ] && [ "${2:-}" = --git-path ]; then exit 1; fi
if [ "$OBS_FAULT" = changes ] && [ "${1:-}" = diff-tree ]; then
if [ "$OBS_FAULT" = changes ] &&
{ [ "${1:-}" = diff-tree ] ||
{ [ "${1:-}" = -c ] && [ "${2:-}" = core.fsmonitor=false ] && [ "${3:-}" = diff-tree ]; }; }; then
printf '.claude-plugin/marketplace.json\0.github/plugin/marketplace.json\0content'
exit 0
fi
Expand Down
74 changes: 74 additions & 0 deletions scripts/marketplace-publication-identity.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Run the publication workflow's actual assessment shell against an advanced checkout.
set -euo pipefail
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
awk '
/^ assess:/ {assessment=1}
/^ publish:/ {assessment=0}
assessment && /^ id: prepare$/ {selected=1}
selected && /^ run: \|$/ {reading=1; next}
reading && /^ / {sub(/^ /,""); print; next}
reading {exit}
' "$root/.github/workflows/publish-marketplace-release.yaml" > "$work/step"
[[ -s $work/step ]] || { echo 'FAIL: assessment step is unavailable' >&2; exit 1; }
git init -q "$work/repo"
git -C "$work/repo" config user.name 'Publication identity fixture'
git -C "$work/repo" config user.email fixture@example.invalid
git -C "$work/repo" config commit.gpgsign false
git -C "$work/repo" commit --allow-empty -qm baseline
dispatch=$(git -C "$work/repo" rev-parse HEAD)
git -C "$work/repo" commit --allow-empty -qm current
current=$(git -C "$work/repo" rev-parse HEAD)
mkdir "$work/repo/scripts" "$work/bin"
cat > "$work/repo/scripts/prepare-merged-marketplace-release.sh" <<'STUB'
#!/usr/bin/env bash
set -euo pipefail
printf 'called\n' >> "$CALLED"
release= ci=
while (($#)); do
case $1 in
--release) release=$2; shift 2 ;;
--ci-run) ci=$2; shift 2 ;;
--repo|--output) shift 2 ;;
*) exit 91 ;;
esac
done
[[ $release == "$CURRENT" && $ci == latest ]] || exit 92
jq -n --arg release "$release" '{status:"NO_VERSION_CHANGE",releaseCommit:$release,ciRunId:42}'
STUB
cat > "$work/bin/git" <<'STUB'
#!/usr/bin/env bash
if [[ ${FAIL_HEAD:-false} == true && " $* " == *' rev-parse '* ]]; then
printf '%s\n' "$CURRENT"
exit 73
fi
exec "$REAL_GIT" "$@"
STUB
chmod +x "$work/bin/git"
real_git=$(command -v git)
pass=0 fail=0
for mode in ordinary queued wrong-ci failed-head; do
: > "$work/called"; : > "$work/outputs"; : > "$work/summary"
selected=$current; [[ $mode != queued ]] || selected=$dispatch
ci=latest; [[ $mode != wrong-ci ]] || ci=41
failed=false; [[ $mode != failed-head ]] || failed=true
status=0
(cd "$work/repo" && CALLED="$work/called" CURRENT="$current" REAL_GIT="$real_git" \
FAIL_HEAD=$failed PATH="$work/bin:$PATH" RELEASE_COMMIT="$selected" CI_RUN=$ci \
REPOSITORY=example/catalogue RUNNER_TEMP="$work" GITHUB_OUTPUT="$work/outputs" \
GITHUB_STEP_SUMMARY="$work/summary" bash -euo pipefail "$work/step") \
> "$work/out" 2> "$work/error" || status=$?
ok=false
case $mode in
ordinary|queued)
if [[ $status == 0 ]] && grep -qx "release=$current" "$work/outputs"; then ok=true; fi ;;
wrong-ci) [[ $status != 0 && ! -s $work/outputs ]] && ok=true ;;
failed-head) [[ $status != 0 && ! -s $work/called && ! -s $work/outputs ]] && ok=true ;;
esac
if [[ $ok == true ]]; then printf 'PASS: publication identity %s\n' "$mode"; pass=$((pass+1))
else printf 'FAIL: publication identity %s (exit %s)\n' "$mode" "$status"; fail=$((fail+1)); fi
done
printf 'Publication identity controls: %s pass, %s fail\n' "$pass" "$fail"
[[ $fail == 0 ]]
2 changes: 1 addition & 1 deletion scripts/propose-marketplace-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ ci_snapshot() {
fi
gh api --hostname github.com "repos/$repo/actions/runs/$ci" > "$temp/ci"
json_object_unique "$temp/ci" || fail 'ambiguous CI observation'
jq -es --arg repo "$repo" --arg source "$source" --argjson ci "$ci" 'length==1 and (.[0]|.id==$ci and .path==".github/workflows/ci.yaml" and .event=="push" and .status=="completed" and .conclusion=="success" and .head_branch=="main" and .head_sha==$source and .repository.full_name==$repo and .head_repository.full_name==$repo)' "$temp/ci" >/dev/null || fail 'exact successful main CI is required'
jq -es --arg repo "$repo" --arg source "$source" --argjson ci "$ci" 'length==1 and (.[0]|.id==$ci and (.path==".github/workflows/ci.yaml" or .path==".github/workflows/ci.yaml@main" or .path==".github/workflows/ci.yaml@refs/heads/main") and .event=="push" and .status=="completed" and .conclusion=="success" and .head_branch=="main" and .head_sha==$source and .repository.full_name==$repo and .head_repository.full_name==$repo)' "$temp/ci" >/dev/null || fail 'exact successful main CI is required'
}
# Refuse any local tag-object change since candidate preparation.
local_tags_unchanged() {
Expand Down
17 changes: 16 additions & 1 deletion scripts/propose-marketplace-release.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,18 @@ elif [ "$endpoint" = repos/example/catalogue/actions/runs/42 ]; then
if [ "$mode" = duplicate-ci ]; then
jq -c . "$FORGE_STATE/ci" | sed 's/^{/{"conclusion":"failure",/'; exit 0
fi
case "$mode" in ci-pending) jq '.status="in_progress"|.conclusion=null' "$FORGE_STATE/ci";; ci-foreign) jq '.head_repository.full_name="other/catalogue"' "$FORGE_STATE/ci";; ci-stale) jq '.head_sha="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' "$FORGE_STATE/ci";; ci-event) jq '.event="workflow_dispatch"' "$FORGE_STATE/ci";; ci-workflow) jq '.path=".github/workflows/other.yaml"' "$FORGE_STATE/ci";; *) cat "$FORGE_STATE/ci";; esac
case "$mode" in
ci-qualified-main) jq '.path=".github/workflows/ci.yaml@main"' "$FORGE_STATE/ci";;
ci-qualified-full-main) jq '.path=".github/workflows/ci.yaml@refs/heads/main"' "$FORGE_STATE/ci";;
ci-qualified-other) jq '.path=".github/workflows/other.yaml@main"' "$FORGE_STATE/ci";;
ci-qualified-feature) jq '.path=".github/workflows/ci.yaml@feature"' "$FORGE_STATE/ci";;
ci-pending) jq '.status="in_progress"|.conclusion=null' "$FORGE_STATE/ci";;
ci-foreign) jq '.head_repository.full_name="other/catalogue"' "$FORGE_STATE/ci";;
ci-stale) jq '.head_sha="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' "$FORGE_STATE/ci";;
ci-event) jq '.event="workflow_dispatch"' "$FORGE_STATE/ci";;
ci-workflow) jq '.path=".github/workflows/other.yaml"' "$FORGE_STATE/ci";;
*) cat "$FORGE_STATE/ci";;
esac
elif [ "$endpoint" = graphql ] && [ "$paginated" = true ]; then
count=$(cat "$FORGE_STATE/reads" 2>/dev/null || printf 0)
count=$((count+1)); printf '%s\n' "$count" > "$FORGE_STATE/reads"
Expand Down Expand Up @@ -275,6 +286,10 @@ run_case() {
passed=$((passed+1))
}
run_case 'read-only preparation' none false PREPARED
run_case 'native main-qualified CI path' ci-qualified-main false PREPARED
run_case 'native fully-qualified main CI path' ci-qualified-full-main false PREPARED
run_case 'qualified unrelated workflow refuses' ci-qualified-other false REFUSED
run_case 'qualified feature workflow refuses' ci-qualified-feature false REFUSED
run_case 'explicit signed draft creation' none true CREATED
run_case 'explicit empty GraphQL errors remain valid' empty-errors true CREATED
for fault in null-errors commit-null-errors readback-null-errors; do run_case "$fault is refused" "$fault" true REFUSED; done
Expand Down
3 changes: 2 additions & 1 deletion scripts/verify-marketplace-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ else
[ "$parents" = "$source" ] || fail 'release must have exactly the selected source as its only parent'
fi
# Only the marketplace manifests may change. Include mode changes, deletions and renames.
git diff-tree --no-relative --no-commit-id --name-only -r --no-renames --no-ext-diff -z "$source" "$release" > "$temp/changes"
# Git can consult the caller index even for tree diffs; keep configured observation hooks inert.
git -c core.fsmonitor=false diff-tree --no-relative --no-commit-id --name-only -r --no-renames --no-ext-diff -z "$source" "$release" > "$temp/changes"
path=''
while IFS= read -r -d '' path; do
case "$path" in
Expand Down
18 changes: 17 additions & 1 deletion scripts/verify-marketplace-release.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ exec "$REAL_FIND" "$@"
STUB
cat > "$work/bin/git" <<'STUB'
#!/usr/bin/env bash
[[ ${DIFF_MODE:-} != empty || $1 != diff-tree ]] || exit 0
[[ ${DIFF_MODE:-} != empty || " $* " != *' diff-tree '* ]] || exit 0
exec "$REAL_GIT" "$@"
STUB
chmod +x "$work/bin/find" "$work/bin/git"
Expand All @@ -80,6 +80,22 @@ git -C "$repo" add content; git -C "$repo" commit --amend --no-edit -q
release=$(git -C "$repo" rev-parse HEAD)
PATH="$work/bin:$PATH" DIFF_MODE=empty REAL_GIT="$real_git" REAL_FIND="$real_find" reject 'empty successful diff inventory cannot hide unrelated content'
incremental; accept 'single-parent manifest-only release' 1.3.0
# Observation must not execute a checkout-configured filesystem hook.
export FS_MARKER="$work/fsmonitor-called"
cat > "$work/fsmonitor" <<'HOOK'
#!/bin/sh
printf 'observed\n' >> "$FS_MARKER"
exit 1
HOOK
chmod +x "$work/fsmonitor"
git -C "$repo" config core.fsmonitor "$work/fsmonitor"
cp "$repo/.git/index" "$work/index-before"
cp "$repo/.git/config" "$work/config-before"
accept 'filesystem observation hook is inert' 1.3.0
[ ! -e "$FS_MARKER" ] || fail 'verification executed a filesystem observation hook'
cmp -s "$repo/.git/index" "$work/index-before" || fail 'verification changed the caller index'
cmp -s "$repo/.git/config" "$work/config-before" || fail 'verification changed the caller configuration'
passed=$((passed+1))
incremental
unusual_repo="$repo$(printf '\001'):quoted\\path"
mv "$repo" "$unusual_repo"; repo="$unusual_repo"
Expand Down
Loading