Skip to content

Commit 2ec989a

Browse files
committed
Splitting ref and sha into two fields correctly for the intake form
1 parent 471895c commit 2ec989a

5 files changed

Lines changed: 78 additions & 26 deletions

File tree

.github/ISSUE_TEMPLATE/external-plugin.yml

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ body:
1414
Before you continue:
1515
- Public submissions are **GitHub-only** in v1.
1616
- The plugin must live in a **public GitHub repository**.
17-
- Use an **immutable ref** for review: a release tag or full 40-character commit SHA.
17+
- Provide an immutable **ref**, **sha**, or both for review.
1818
- Do **not** open a PR that edits `plugins/external.json` directly.
1919
- type: input
2020
id: plugin-name
@@ -51,11 +51,19 @@ body:
5151
- type: input
5252
id: immutable-ref
5353
attributes:
54-
label: Immutable ref to review
55-
description: Release tag or full 40-character commit SHA.
56-
placeholder: refs/tags/v1.2.3 or 0123456789abcdef0123456789abcdef01234567
54+
label: Ref to review
55+
description: Optional release tag or tag ref. Submit this, a commit SHA, or both.
56+
placeholder: v1.2.3
5757
validations:
58-
required: true
58+
required: false
59+
- type: input
60+
id: immutable-sha
61+
attributes:
62+
label: Commit SHA to review
63+
description: Optional full 40-character commit SHA. Submit this, a ref, or both.
64+
placeholder: 0123456789abcdef0123456789abcdef01234567
65+
validations:
66+
required: false
5967
- type: input
6068
id: version
6169
attributes:
@@ -119,7 +127,7 @@ body:
119127
options:
120128
- label: The plugin lives in a public GitHub repository.
121129
required: true
122-
- label: The ref I provided is an immutable release tag or full 40-character commit SHA, not a branch.
130+
- label: The ref and/or sha I provided is immutable (release tag and/or full 40-character commit SHA), not a branch.
123131
required: true
124132
- label: This submission follows this repository's contribution, security, and responsible AI policies.
125133
required: true

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin:
164164

165165
1. Do not open a direct PR that edits `plugins/external.json` for a public third-party plugin submission
166166
2. Public external plugin submissions use the external plugin issue workflow documented in [CONTRIBUTING.md](CONTRIBUTING.md#adding-external-plugins)
167-
3. In v1, only GitHub-hosted plugins are accepted for public submission, using a public repo plus an immutable `ref`
167+
3. In v1, only GitHub-hosted plugins are accepted for public submission, using a public repo plus an immutable `ref`, `sha`, or both
168168
4. The shared validator in `eng/external-plugin-validation.mjs` is the canonical source of truth for external plugin data rules; reuse it instead of duplicating checks in scripts or workflows
169169
5. Submission issues move through `external-plugin` + `awaiting-review` -> `ready-for-review` -> `approved` or `rejected`
170170
6. Maintainers make the decision with `/approve` or `/reject <reason>` issue comments; approved issues are closed and used as the six-month re-review anchor

CONTRIBUTING.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,15 +202,16 @@ The external plugin issue form will collect these fields:
202202
- Short description
203203
- GitHub repository in `owner/repo` format
204204
- Plugin path inside the repository (optional when the plugin is at the repository root)
205-
- Immutable ref to review (`ref`), using a release tag or full commit SHA rather than a branch
205+
- Ref to review (`ref`), using a release tag or tag ref rather than a branch
206+
- Commit SHA to review (`sha`), using a full 40-character commit SHA
206207
- Plugin version
207208
- License identifier
208209
- Author name
209210
- Author URL (optional)
210211
- Homepage URL (optional)
211212
- Keywords/tags
212213
- Additional notes for reviewers (optional)
213-
- Confirmation checkboxes that the repository is public, the ref is immutable, the submission follows this repository's policies, and the plugin is not a duplicate listing
214+
- Confirmation checkboxes that the repository is public, the submitted ref and/or sha is immutable, the submission follows this repository's policies, and the plugin is not a duplicate listing
214215

215216
The repository's canonical validation rules live in `eng/external-plugin-validation.mjs`. Build scripts reuse the `marketplace` policy from that module, and the issue intake automation uses the stricter `publicSubmission` policy so the JSON contract and workflow checks stay aligned.
216217

@@ -223,7 +224,7 @@ For entries committed to `plugins/external.json`, the current marketplace valida
223224
- `source.source: "github"` plus `source.repo` in `owner/repo` format
224225
- optional `source.path` values to stay relative to the repository root
225226

226-
The public-submission policy builds on those rules and also requires `license` plus an immutable `source.ref`.
227+
The public-submission policy builds on those rules and also requires `license` plus at least one immutable source locator: `source.ref`, `source.sha`, or both.
227228

228229
##### Review workflow
229230

@@ -239,7 +240,7 @@ The public-submission policy builds on those rules and also requires `license` p
239240
Maintainers are responsible for confirming that the submission:
240241

241242
- Clearly fits the Awesome Copilot collection and adds value beyond existing listings
242-
- Uses a public GitHub repository and an immutable ref that can be reviewed reliably
243+
- Uses a public GitHub repository and an immutable ref and/or SHA that can be reviewed reliably
243244
- Includes the required metadata for `plugins/external.json` (`name`, `description`, `version`, `author.name`, `repository`, `keywords`, and `source`), plus any supplied homepage/license fields
244245
- Does not obviously duplicate an existing marketplace entry
245246
- Continues to meet this repository's content, security, and responsible AI policies
@@ -283,7 +284,8 @@ Approved submissions are converted into `plugins/external.json` entries followin
283284
"source": "github",
284285
"repo": "owner/plugin-repo",
285286
"path": ".github/plugins/my-external-plugin",
286-
"ref": "v1.0.0"
287+
"ref": "v1.0.0",
288+
"sha": "0123456789abcdef0123456789abcdef01234567"
287289
}
288290
}
289291
]

eng/external-plugin-intake.mjs

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
1111

1212
const REQUIRED_CHECKLIST_ITEMS = [
1313
"The plugin lives in a public GitHub repository.",
14-
"The ref I provided is an immutable release tag or full 40-character commit SHA, not a branch.",
14+
"The ref and/or sha I provided is immutable (release tag and/or full 40-character commit SHA), not a branch.",
1515
"This submission follows this repository's contribution, security, and responsible AI policies.",
1616
"This plugin is not already listed in the Awesome Copilot marketplace.",
1717
];
@@ -21,7 +21,8 @@ const FIELD_TITLES = Object.freeze({
2121
shortDescription: "Short description",
2222
githubRepository: "GitHub repository",
2323
pluginPath: "Plugin path inside the repository",
24-
immutableRef: "Immutable ref to review",
24+
immutableRef: "Ref to review",
25+
immutableSha: "Commit SHA to review",
2526
version: "Version",
2627
license: "License identifier",
2728
authorName: "Author name",
@@ -150,7 +151,7 @@ function encodeRepoPath(repo) {
150151
return `${encodeURIComponent(owner ?? "")}/${encodeURIComponent(name ?? "")}`;
151152
}
152153

153-
async function validateRemoteRepository(repo, ref, errors, warnings, token) {
154+
async function validateRemoteRepository(repo, { ref, sha }, errors, warnings, token) {
154155
const encodedRepo = encodeRepoPath(repo);
155156
const repositoryResponse = await fetchGitHubJson(`/repos/${encodedRepo}`, token);
156157

@@ -171,6 +172,15 @@ async function validateRemoteRepository(repo, ref, errors, warnings, token) {
171172
warnings.push(`submission: GitHub repository "${repo}" is archived`);
172173
}
173174

175+
if (sha) {
176+
if (/^[0-9a-f]{40}$/i.test(sha)) {
177+
const commitResponse = await fetchGitHubJson(`/repos/${encodedRepo}/commits/${encodeURIComponent(sha)}`, token);
178+
if (!commitResponse.ok) {
179+
errors.push(`submission: commit "${sha}" was not found in GitHub repository "${repo}"`);
180+
}
181+
}
182+
}
183+
174184
if (!ref) {
175185
return;
176186
}
@@ -183,6 +193,14 @@ async function validateRemoteRepository(repo, ref, errors, warnings, token) {
183193
return;
184194
}
185195

196+
if (ref.startsWith("refs/heads/") || ["main", "master", "develop", "development", "dev", "trunk"].includes(ref)) {
197+
return;
198+
}
199+
200+
if (ref.startsWith("refs/") && !ref.startsWith("refs/tags/")) {
201+
return;
202+
}
203+
186204
const tagName = ref.startsWith("refs/tags/") ? ref.slice("refs/tags/".length) : ref;
187205
const tagResponse = await fetchGitHubJson(`/repos/${encodedRepo}/git/ref/tags/${encodeURIComponent(tagName)}`, token);
188206

@@ -191,7 +209,7 @@ async function validateRemoteRepository(repo, ref, errors, warnings, token) {
191209
}
192210

193211
if (/^[0-9a-f]+$/i.test(ref) && ref.length !== 40) {
194-
errors.push('submission: commit SHAs in "Immutable ref to review" must use the full 40-character SHA');
212+
errors.push('submission: commit SHAs in "Ref to review" must use the full 40-character SHA or be submitted in "Commit SHA to review"');
195213
return;
196214
}
197215

@@ -215,7 +233,8 @@ export function parseExternalPluginIssueBody(body) {
215233
const pluginName = requiredField(FIELD_TITLES.pluginName);
216234
const shortDescription = requiredField(FIELD_TITLES.shortDescription);
217235
const repoInput = normalizeGitHubRepo(requiredField(FIELD_TITLES.githubRepository));
218-
const immutableRef = requiredField(FIELD_TITLES.immutableRef);
236+
const immutableRef = stripNoResponse(sections.get(FIELD_TITLES.immutableRef));
237+
const immutableSha = stripNoResponse(sections.get(FIELD_TITLES.immutableSha));
219238
const version = requiredField(FIELD_TITLES.version);
220239
const license = requiredField(FIELD_TITLES.license);
221240
const authorName = requiredField(FIELD_TITLES.authorName);
@@ -227,6 +246,10 @@ export function parseExternalPluginIssueBody(body) {
227246
const additionalNotes = stripNoResponse(sections.get(FIELD_TITLES.additionalNotes));
228247
const checkedItems = parseChecklist(sections.get(FIELD_TITLES.submissionChecklist));
229248

249+
if (!immutableRef && !immutableSha) {
250+
errors.push(`submission: one of "${FIELD_TITLES.immutableRef}" or "${FIELD_TITLES.immutableSha}" is required`);
251+
}
252+
230253
for (const item of REQUIRED_CHECKLIST_ITEMS) {
231254
if (!checkedItems.has(item)) {
232255
errors.push(`submission: checklist item must be checked: "${item}"`);
@@ -250,6 +273,7 @@ export function parseExternalPluginIssueBody(body) {
250273
repo: repoInput,
251274
...(pluginPath ? { path: pluginPath } : {}),
252275
...(immutableRef ? { ref: immutableRef } : {}),
276+
...(immutableSha ? { sha: immutableSha } : {}),
253277
},
254278
};
255279

@@ -287,8 +311,8 @@ export async function evaluateExternalPluginIssue({ issue, token } = {}) {
287311
}
288312
}
289313

290-
if (parsed.plugin?.source?.repo && parsed.plugin?.source?.ref) {
291-
await validateRemoteRepository(parsed.plugin.source.repo, parsed.plugin.source.ref, errors, warnings, token);
314+
if (parsed.plugin?.source?.repo && (parsed.plugin?.source?.ref || parsed.plugin?.source?.sha)) {
315+
await validateRemoteRepository(parsed.plugin.source.repo, parsed.plugin.source, errors, warnings, token);
292316
}
293317

294318
const dedupedErrors = [...new Set(errors)];
@@ -314,7 +338,8 @@ export async function evaluateExternalPluginIssue({ issue, token } = {}) {
314338
"",
315339
`- **Plugin:** ${parsed.plugin.name}`,
316340
`- **Repository:** ${parsed.plugin.repository}`,
317-
`- **Ref:** ${parsed.plugin.source.ref}`,
341+
parsed.plugin.source.ref ? `- **Ref:** ${parsed.plugin.source.ref}` : undefined,
342+
parsed.plugin.source.sha ? `- **SHA:** ${parsed.plugin.source.sha}` : undefined,
318343
`- **Keywords:** ${normalizedKeywords}`,
319344
"",
320345
"### Canonical external.json payload",

eng/external-plugin-validation.mjs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ export const EXTERNAL_PLUGIN_POLICIES = Object.freeze({
1111
requireRepository: true,
1212
requireKeywords: true,
1313
requireLicense: false,
14-
requireImmutableRef: false,
14+
requireImmutableLocator: false,
1515
}),
1616
publicSubmission: Object.freeze({
1717
allowedSourceTypes: ["github"],
1818
requireAuthor: true,
1919
requireRepository: true,
2020
requireKeywords: true,
2121
requireLicense: true,
22-
requireImmutableRef: true,
22+
requireImmutableLocator: true,
2323
}),
2424
});
2525

@@ -241,7 +241,18 @@ function validateImmutableRef(ref, prefix, errors) {
241241
}
242242
}
243243

244-
function validateGitHubSource(source, prefix, errors, requireImmutableRef) {
244+
function validateCommitSha(sha, prefix, errors) {
245+
if (!isNonEmptyString(sha)) {
246+
errors.push(`${prefix}: "source.sha" must be a non-empty string when provided`);
247+
return;
248+
}
249+
250+
if (!/^[0-9a-f]{40}$/i.test(sha)) {
251+
errors.push(`${prefix}: "source.sha" must be a full 40-character commit SHA`);
252+
}
253+
}
254+
255+
function validateGitHubSource(source, prefix, errors, requireImmutableLocator) {
245256
if (!source || typeof source !== "object" || Array.isArray(source)) {
246257
errors.push(`${prefix}: "source" must be an object`);
247258
return;
@@ -263,8 +274,14 @@ function validateGitHubSource(source, prefix, errors, requireImmutableRef) {
263274

264275
if (source.ref !== undefined) {
265276
validateImmutableRef(source.ref, prefix, errors);
266-
} else if (requireImmutableRef) {
267-
errors.push(`${prefix}: "source.ref" is required for public external plugin submissions`);
277+
}
278+
279+
if (source.sha !== undefined) {
280+
validateCommitSha(source.sha, prefix, errors);
281+
}
282+
283+
if (requireImmutableLocator && source.ref === undefined && source.sha === undefined) {
284+
errors.push(`${prefix}: one of "source.ref" or "source.sha" is required for public external plugin submissions`);
268285
}
269286
}
270287

@@ -301,7 +318,7 @@ export function validateExternalPlugin(plugin, index, options = {}) {
301318
} else if (!policy.allowedSourceTypes.includes(plugin.source.source)) {
302319
errors.push(`${prefix}: "source.source" must be one of: ${policy.allowedSourceTypes.join(", ")}`);
303320
} else if (plugin.source.source === "github") {
304-
validateGitHubSource(plugin.source, prefix, errors, policy.requireImmutableRef);
321+
validateGitHubSource(plugin.source, prefix, errors, policy.requireImmutableLocator);
305322
}
306323

307324
return { errors, warnings };

0 commit comments

Comments
 (0)