Skip to content

Add automated template submission pipeline for trusted publishers#785

Closed
jongio wants to merge 4 commits intoAzure:mainfrom
jongio:template-submission-automation
Closed

Add automated template submission pipeline for trusted publishers#785
jongio wants to merge 4 commits intoAzure:mainfrom
jongio:template-submission-automation

Conversation

@jongio
Copy link
Copy Markdown
Member

@jongio jongio commented Mar 26, 2026

Summary

Mirrors the existing extension submission automation for templates. Trusted internal partners can now submit templates via a GitHub issue form instead of manually editing the 5500-line templates.json. The workflow validates the submission, generates the JSON entry, and creates a PR automatically. Trusted publishers get auto-merge after CI passes.

Closes #784

Components

File Purpose
.github/ISSUE_TEMPLATE/template-submission.yml Structured issue form for template submissions
website/scripts/validate-template.js Validates source repo URL, checks repo exists, generates UUID
.github/workflows/template-submission.yml Parses issue, validates, updates templates.json, creates PR
.github/trusted-publishers.json Allowlist of GitHub usernames for auto-merge
.github/workflows/template-auto-merge.yml Auto-merges trusted publisher PRs after CI passes
website/docs/contribute.md Updated contributor guide with automated submission path

How It Works

For any contributor:

  1. Fill out the template submission issue form
  2. Workflow auto-validates the repo and creates a PR with the templates.json entry
  3. Maintainers review and merge

For trusted publishers (listed in trusted-publishers.json):

  1. Same flow, but the PR gets auto-approved and auto-merged after CI passes
  2. Time from submission to live: minutes instead of days

Security Model

  • Trusted publishers list is version-controlled (changes require PR review)
  • Auto-merge workflow double-checks PR author against trusted list (does not trust labels alone)
  • URL validation prevents SSRF (only HTTPS GitHub URLs accepted)
  • Everything is rollbackable via git revert
  • Branch protection rules still apply

Testing

  • npm test -- 15/15 tests pass (0 failures)
  • npm run build -- builds successfully
  • Validation script handles edge cases (invalid URLs, non-GitHub URLs, missing repos)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces GitHub-issue-driven automation for adding azd templates to the gallery (validation → templates.json update → PR creation), with an auto-merge path for allowlisted publishers. In addition, it includes a broad website redesign (new design tokens, gallery/filters UX updates, getting-started refresh), new service landing pages, and new Playwright e2e scaffolding.

Changes:

  • Add template submission automation via issue form + workflow + repo validation script, plus trusted-publisher auto-merge workflow.
  • Implement a site-wide visual refresh (design tokens, updated gallery hero/search/filters/cards, updated getting-started content).
  • Add service landing page framework + 2 example service pages, and introduce Playwright configuration + initial e2e specs.

Reviewed changes

Copilot reviewed 48 out of 49 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
.github/ISSUE_TEMPLATE/template-submission.yml Adds an issue form used to collect template submission metadata.
.github/trusted-publishers.json Adds an allowlist of trusted GitHub usernames for expedited handling.
.github/workflows/template-submission.yml Adds issue/dispatch workflow to parse submissions, validate, update templates.json, and open a PR.
.github/workflows/template-auto-merge.yml Adds auto-approval/auto-merge workflow for trusted publisher submissions.
website/scripts/validate-template.js Adds a Node script to validate a GitHub repo URL via GitHub API and generate a UUID.
website/docs/contribute.md Documents the new automated template submission path.
website/package.json Adds Playwright test dependency.
website/package-lock.json Locks Playwright (and related) dependency tree changes.
website/playwright.config.ts Adds Playwright runner configuration for e2e tests.
website/e2e/navigation-a11y.spec.ts Adds navigation + basic accessibility smoke tests.
website/e2e/homepage.spec.ts Adds homepage e2e assertions for hero/search/cards/navbar.
website/e2e/getting-started.spec.ts Adds getting-started page e2e assertions.
website/e2e/gallery-filters.spec.ts Adds gallery filtering and empty-state e2e coverage.
website/docusaurus.config.js Adds Google Fonts, adds “Services” dropdown, and updates primary CTA label.
website/src/css/custom.css Introduces new design tokens (palette/type/spacing) and global styling adjustments.
website/src/pages/styles.module.css Updates homepage/gallery page layout/styling (hero/search/filter/card spacing & colors).
website/src/components/gallery/ShowcaseTemplateSearch/index.tsx Refreshes gallery hero content, adds stats bar and a templates CTA, improves search accessibility.
website/src/components/gallery/ShowcaseTemplateSearch/styles.module.css Adds new hero/search CTA/stats styling for the gallery.
website/src/components/gallery/ShowcaseLeftFilters/index.tsx Adds per-tag counts, improves keyboard accessibility for “Clear all”, and updates chevron images.
website/src/components/gallery/ShowcaseLeftFilters/styles.module.css Updates filter styling and adds count badge styling.
website/src/components/gallery/ShowcaseTagSelect/index.tsx Adds optional count display next to filter labels.
website/src/components/gallery/ShowcaseEmptyResult/index.tsx Updates empty-state messaging and adds image width attribute.
website/src/components/gallery/ShowcaseEmptyResult/styles.module.css Updates empty-state link color to the new primary token.
website/src/components/gallery/ShowcaseCard/index.tsx Adds image sizing/aria labels and tweaks title interaction semantics.
website/src/components/gallery/ShowcaseCard/styles.module.css Redesigns template cards (surface/border/shadow/type/inputs).
website/src/components/gallery/ShowcaseCardPanel/index.tsx Improves button semantics/aria-labels and sets icon image dimensions.
website/src/components/gallery/ShowcaseCardPanel/styles.module.css Updates card panel accent color to new primary.
website/src/components/gallery/ShowcaseExtensionCard/styles.module.css Aligns extension card styling with new tokens (radius/colors/fonts).
website/src/components/gallery/ShowcaseSurveyCard/styles.module.css Updates survey card/button styling to new tokens.
website/src/components/gallery/ShowcaseMultipleAuthors/index.tsx Replaces var with const for split author arrays.
website/src/components/gallery/ShowcaseMultipleAuthors/styles.module.css Updates author link colors to new primary token.
website/src/components/styles.module.css Updates dark theme card/content colors.
website/src/pages/ShowcaseCardPage.tsx Updates submission link color token.
website/src/pages/getting-started/index.js Updates getting-started hero copy, adds 3-step section, and tweaks video iframe attributes/title.
website/src/pages/getting-started/styles.module.css Redesigns getting-started layout and adds styles for steps + featured section.
website/src/components/ServiceLandingPage/index.tsx Adds a reusable service landing page component that filters templates by service tag.
website/src/components/ServiceLandingPage/styles.module.css Adds styles for service landing pages (hero, quickstart, templates, resources).
website/src/pages/services/container-apps.tsx Adds Azure Container Apps service landing page config.
website/src/pages/services/azure-functions.tsx Adds Azure Functions service landing page config.
website/src/components/Feature.js Updates feature link color styling to match new palette.
docs/spec.md Adds a redesign product spec document.
baseline/sitemap.json Adds baseline sitemap artifact.
baseline/report.md Adds baseline site report artifact.
baseline/design-system.md Adds baseline design system artifact.
baseline/design-inventory.json Adds baseline design inventory artifact.
baseline/content-architecture.md Adds baseline information architecture artifact.
.ai-team/team.md Adds AI team/roles documentation for redesign work.
.ai-team/routing.md Adds routing rules documentation for redesign work.
.ai-team/decisions.md Adds decisions log documentation for redesign work.
Files not reviewed (1)
  • website/package-lock.json: Language not supported

@jongio jongio force-pushed the template-submission-automation branch from cf2e33c to 740dd96 Compare March 26, 2026 22:24
@jongio
Copy link
Copy Markdown
Member Author

jongio commented Mar 26, 2026

Addressing Copilot Review Feedback

Thread 1 — Scope mismatch (.github/workflows/template-submission.yml): This was a false positive from the initial push before rebasing. The PR now contains only the 6 template submission automation files — no redesign or unrelated changes. The reviewer saw the old commit (cf2e33c) which included local commits that have since been cleaned up via rebase.

Thread 2 — HTTP vs HTTPS validation (validate-template.js): ✅ Fixed in e87c3df. The validator now rejects http: URLs and requires https: only, matching the security model described in the spec.

Thread 3 — Auto-merge checks wrong identity (template-auto-merge.yml): ✅ Fixed in e87c3df. The workflow now parses the originating issue number from the PR body (from issue #NNN pattern) and verifies the issue submitter against trusted-publishers.json, rather than the PR author (which would be github-actions[bot]).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.

Comment on lines +50 to +54
// Validate it is a GitHub URL
if (
parsed.hostname !== "github.com" &&
parsed.hostname !== "www.github.com"
) {
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script accepts www.github.com but returns sourceUrl unchanged. Since the workflow stores this value, equivalent URLs can bypass dedupe (e.g., www.github.com/... vs github.com/..., .git suffix). Consider returning a canonicalized repo-root URL (normalized host, no .git, no trailing slash).

Copilot uses AI. Check for mistakes.
const prBody = context.payload.pull_request.body || '';
const issueMatch = prBody.match(/from issue #(\d+)/);
if (!issueMatch) {
core.setFailed('Could not determine originating issue from PR body');
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verify trusted publisher fails if it can't parse from issue #NNN from the PR body. PRs created via workflow_dispatch intentionally omit the issue reference, but template-submission.yml can still add the trusted-publisher label—leading to auto-merge failures for dispatch-created PRs. Either avoid adding trusted-publisher for workflow_dispatch, or include a verifiable origin identifier in the PR body.

Suggested change
core.setFailed('Could not determine originating issue from PR body');
core.info('No originating issue reference found in PR body; skipping trusted publisher verification for this PR.');

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +70
// Extract owner/repo from path
const pathParts = parsed.pathname.replace(/^\//, "").replace(/\/$/, "").replace(/\.git$/, "").split("/");
if (pathParts.length < 2 || !pathParts[0] || !pathParts[1]) {
errors.push(
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Owner/repo extraction accepts URLs with extra path segments (any path with >=2 segments), even though the error message says the expected format is https://github.com/{owner}/{repo}. This can lead to storing non-canonical source URLs and makes deduping harder. Consider rejecting paths with extra segments and/or canonicalizing the stored URL to the repo root.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +23
uses: actions/checkout@v4

- name: Verify trusted publisher
id: verify
uses: actions/github-script@v7
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow pins older action majors (actions/checkout@v4, actions/github-script@v7) while the repo’s other workflows use checkout v6 and github-script v8. Aligning versions across workflows reduces maintenance burden and keeps security/bugfix updates consistent.

Suggested change
uses: actions/checkout@v4
- name: Verify trusted publisher
id: verify
uses: actions/github-script@v7
uses: actions/checkout@v6
- name: Verify trusted publisher
id: verify
uses: actions/github-script@v8

Copilot uses AI. Check for mistakes.
Comment on lines +286 to +287
const additionalTags = parseCsvTags(process.env.TPL_ADDITIONAL_TAGS);
for (const tag of additionalTags) {
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

additional_tags can include special admin tags like aicollection, but the workflow doesn't validate whether the template is actually in the curated AI collection (per the tag description in website/src/data/tags.tsx). Since trusted publisher PRs can be auto-merged, consider blocking aicollection (and similar admin-only tags) from additional_tags unless you add deterministic validation.

Suggested change
const additionalTags = parseCsvTags(process.env.TPL_ADDITIONAL_TAGS);
for (const tag of additionalTags) {
const additionalTags = parseCsvTags(process.env.TPL_ADDITIONAL_TAGS);
// Certain admin-only tags (e.g., "aicollection") must not be set via additional_tags.
const disallowedAdditionalTags = new Set(['aicollection']);
for (const tag of additionalTags) {
if (disallowedAdditionalTags.has(tag)) {
core.info(`Ignoring disallowed admin tag in additional_tags: "${tag}"`);
continue;
}

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +174
node website/scripts/validate-template.js "$SOURCE_REPO" > validation_result.json 2>validation_errors.log
if [ $? -ne 0 ]; then
echo "valid=false" >> $GITHUB_OUTPUT
{
echo 'errors<<EOF'
cat validation_result.json validation_errors.log
echo 'EOF'
} >> $GITHUB_OUTPUT
else
echo "valid=true" >> $GITHUB_OUTPUT
{
echo 'result<<EOF'
cat validation_result.json
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This run: block assumes it can inspect $? after node ..., but GitHub Actions runs bash with -e by default; when node exits non-zero the step terminates immediately and these outputs won't be written. Wrap the node call in an if node ...; then ... else ... fi (or disable -e around the call) so validation failures can be reported via outputs/comments.

Suggested change
node website/scripts/validate-template.js "$SOURCE_REPO" > validation_result.json 2>validation_errors.log
if [ $? -ne 0 ]; then
echo "valid=false" >> $GITHUB_OUTPUT
{
echo 'errors<<EOF'
cat validation_result.json validation_errors.log
echo 'EOF'
} >> $GITHUB_OUTPUT
else
echo "valid=true" >> $GITHUB_OUTPUT
{
echo 'result<<EOF'
cat validation_result.json
if node website/scripts/validate-template.js "$SOURCE_REPO" > validation_result.json 2>validation_errors.log; then
echo "valid=true" >> $GITHUB_OUTPUT
{
echo 'result<<EOF'
cat validation_result.json
echo 'EOF'
} >> $GITHUB_OUTPUT
else
echo "valid=false" >> $GITHUB_OUTPUT
{
echo 'errors<<EOF'
cat validation_result.json validation_errors.log

Copilot uses AI. Check for mistakes.
Comment on lines +241 to +242
if (!['http:', 'https:'].includes(u.protocol)) {
throw new Error(`unsafe protocol "${u.protocol}"`);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateUrl() allows http: URLs (['http:', 'https:']). Since the site is served over HTTPS, http: preview images can cause mixed-content blocking in browsers, and allowing non-HTTPS URLs weakens the security posture described in the PR. Consider requiring https: for user-supplied URLs here.

Suggested change
if (!['http:', 'https:'].includes(u.protocol)) {
throw new Error(`unsafe protocol "${u.protocol}"`);
if (u.protocol !== 'https:') {
throw new Error(`unsafe protocol "${u.protocol}" (only "https:" is allowed)`);

Copilot uses AI. Check for mistakes.
Comment on lines +272 to +275
// Check for duplicate source URLs
const normalizedSource = sourceRepo.replace(/\/+$/, '').toLowerCase();
const duplicate = templates.find(
t => t.source && t.source.replace(/\/+$/, '').toLowerCase() === normalizedSource
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate detection only normalizes trailing slashes + lowercase. This misses common equivalent forms like a .git suffix or www.github.com host, so the workflow can add duplicate entries for the same repo. Consider canonicalizing the source URL (strip .git, normalize host to github.com, and possibly rewrite to https://github.com/{owner}/{repo}) before comparing/storing.

Suggested change
// Check for duplicate source URLs
const normalizedSource = sourceRepo.replace(/\/+$/, '').toLowerCase();
const duplicate = templates.find(
t => t.source && t.source.replace(/\/+$/, '').toLowerCase() === normalizedSource
/**
* Canonicalize a repository source URL for duplicate detection.
* - Trims whitespace
* - Removes trailing slashes
* - Strips a trailing `.git`
* - Lowercases the result
* - For GitHub URLs, normalizes host to github.com and rewrites to
* https://github.com/{owner}/{repo}
*/
function canonicalizeSourceUrl(input) {
if (!input) {
return '';
}
const trimmed = input.trim();
let urlObj;
try {
urlObj = new URL(trimmed);
} catch {
// If it's not a valid URL, fall back to basic normalization
return trimmed
.replace(/\/+$/, '')
.replace(/\.git$/i, '')
.toLowerCase();
}
const hostname = urlObj.hostname.toLowerCase();
if (hostname === 'github.com' || hostname === 'www.github.com') {
const parts = urlObj.pathname.replace(/\/+$/, '').split('/').filter(Boolean);
if (parts.length >= 2) {
const owner = parts[0];
const repo = parts[1].replace(/\.git$/i, '');
return `https://github.com/${owner}/${repo}`.toLowerCase();
}
}
// Non-GitHub or unrecognized GitHub path: basic normalization of full URL
let href = urlObj.href;
href = href.replace(/\/+$/, '').replace(/\.git$/i, '');
return href.toLowerCase();
}
// Check for duplicate source URLs
const canonicalSource = canonicalizeSourceUrl(sourceRepo);
const duplicate = templates.find(
t => t.source && canonicalizeSourceUrl(t.source) === canonicalSource

Copilot uses AI. Check for mistakes.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr review ${{ github.event.pull_request.number }} --approve --body "Auto-approved: PR author is a trusted publisher."
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approval review body says "PR author is a trusted publisher", but this workflow actually verifies the originating issue submitter (not the PR author, which is often github-actions[bot]). Update the message to reflect what was verified so the audit trail is accurate.

Suggested change
gh pr review ${{ github.event.pull_request.number }} --approve --body "Auto-approved: PR author is a trusted publisher."
gh pr review ${{ github.event.pull_request.number }} --approve --body "Auto-approved: originating issue submitter is a trusted publisher."

Copilot uses AI. Check for mistakes.

- name: Parse issue body
id: parse
uses: actions/github-script@v7
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow uses older action versions (actions/github-script@v7, peter-evans/create-pull-request@v7) while the repo uses newer major versions elsewhere (e.g., github-script v8 and create-pull-request v8 in .github/workflows/extension-submission.yml). Consider updating to match the repo’s pinned majors for consistency and security fixes.

Suggested change
uses: actions/github-script@v7
uses: actions/github-script@v8

Copilot uses AI. Check for mistakes.
- GitHub Issue form for template submissions with structured fields
- Automated validation workflow: URL checks, HTTPS-only, duplicate detection
- Auto-merge workflow for trusted publisher submissions
- Trusted publishers registry (trusted-publishers.json)
- Template validation script with URL canonicalization
- Updated contribution docs with new submission process
@jongio jongio force-pushed the template-submission-automation branch from e87c3df to a626727 Compare March 27, 2026 22:39
Copy link
Copy Markdown
Contributor

@hemarina hemarina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #785 — Add automated template submission pipeline for trusted publishers

Great work building out this automation pipeline — the issue form design is solid, the SSRF protections in validate-template.js are well-thought-out, and the prior review feedback (HTTP protocol, PR-author verification) was addressed promptly. Below are additional findings that supplement the existing Copilot review.

Summary

Priority Count
Critical 2
High 6
Medium 4
Total 12

Overall Assessment: Request changes

What Looks Good

  • SSRF protection in validate-template.js is well-implemented — HTTPS-only, GitHub hostname validation, encodeURIComponent, AbortController timeout.
  • Issue form design is thorough — clear descriptions, constrained dropdowns, appropriate required/optional flags.
  • Prior review feedback was addressed — HTTP protocol and PR-author bugs fixed in e87c3df.
  • Security model is version-controlled — trusted-publishers.json changes require PR review.

See inline comments for all 12 findings.

}

const issueNumber = parseInt(issueMatch[1], 10);
const { data: issue } = await github.rest.issues.get({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] PR body forgery enables trusted-publisher impersonation

The auto-merge workflow extracts the originating issue number from the PR body via prBody.match(/from issue #(\d+)/), then checks if that issue's creator is a trusted publisher. It never verifies who created the PR or that the PR was generated by the template-submission workflow. A collaborator could create a PR with arbitrary changes, reference a trusted publisher's issue in the body text, and have the trusted-publisher label applied — triggering auto-approval and merge.

Impact: Complete bypass of the trusted-publisher security model.

Suggested Fix: Add checks that verify PR author is github-actions[bot] AND that the PR only modifies website/static/templates.json.

github.event_name == 'workflow_dispatch' ||
contains(github.event.issue.labels.*.name, 'template-submission')
runs-on: ubuntu-latest
steps:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] No concurrency control — lost-update race on templates.json

The workflow performs a non-atomic read-modify-write on templates.json. If two issues are labeled simultaneously, both runs checkout the same base, both append only their entry, and both create PRs. Merging the first makes the second's base stale — merging it silently drops the first entry with no merge conflict.

Impact: Template submissions can be silently lost.

Suggested Fix:

jobs:
  process-template:
    concurrency:
      group: template-submission-mutex
      cancel-in-progress: false

token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Add template: ${{ steps.parse.outputs.title }} from ${{ github.event_name == 'workflow_dispatch' && 'manual dispatch' || format('issue #{0}', github.event.issue.number) }}"
branch: "template-submission-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.event.issue.number }}"
title: "Add template: ${{ steps.parse.outputs.title }}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] GITHUB_TOKEN events won't trigger auto-merge workflow

peter-evans/create-pull-request uses secrets.GITHUB_TOKEN. GitHub prevents events from GITHUB_TOKEN from triggering other workflows. The trusted-publisher label added during PR creation will not fire the pull_request: [labeled] event that auto-merge listens for — making the entire auto-merge flow non-functional as designed.

Suggested Fix: Document that auto-merge requires manual label re-application, or use a GitHub App token with scoped permissions (after implementing PR-author and file-scope safeguards).

}

const config = JSON.parse(fs.readFileSync(trustedPath, 'utf8'));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] Unguarded JSON.parse on trusted-publishers.json risks opaque crash

JSON.parse(fs.readFileSync(trustedPath, 'utf8')) has no try/catch. Malformed JSON produces an unhandled SyntaxError with no diagnostic context. Same issue exists in the template-submission workflow for VALIDATION_RESULT and templates.json.

Suggested Fix: Wrap in try/catch with descriptive core.setFailed() messages.

GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr checks ${{ github.event.pull_request.number }} --watch --fail-fast

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] gh pr checks --watch has no timeout — can hang indefinitely

gh pr checks --watch --fail-fast blocks until all checks complete. A stuck external status check causes this step to hang until the 6-hour Actions job limit, consuming runner minutes with no output.

Suggested Fix: Add timeout-minutes: 30 to this step.

repo: context.repo.repo,
issue_number: issueNumber,
});
const submitter = issue.user.login;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] Unhandled API error from github.rest.issues.get()

No try-catch around this API call. Rate limits (403), server errors (500), or deleted issues throw unhandled Octokit exceptions with opaque stack traces.

Suggested Fix: Wrap in try/catch with core.setFailed() including issue number and error status.

echo "valid=true" >> $GITHUB_OUTPUT
{
echo 'result<<EOF'
cat validation_result.json
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Heredoc EOF delimiter can be injected via user input

Validation errors use EOF as heredoc delimiter for GITHUB_OUTPUT. Crafted input producing stderr containing exactly EOF prematurely closes the heredoc and could inject additional output key-value pairs.

Suggested Fix: Use a random delimiter: DELIMITER=$(uuidgen).

const azureServices = extractField(body, 'Azure Services');
const additionalTags = extractField(body, 'Additional Tags');

if (!sourceRepo || !title || !description || !author || !authorUrl) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Missing input validation for authorType and iacProvider

The required-fields guard checks !sourceRepo || !title || !description || !author || !authorUrl but omits authorType and iacProvider. API-created issues that bypass form validation can submit empty values, causing incorrect tags or toLowerCase() errors downstream.

Suggested Fix: Add || !authorType || !iacProvider to the guard.

author: process.env.TPL_AUTHOR,
source: sourceRepo,
tags: tags,
id: validation.generatedId
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] No guard on validation.generatedId

id: validation.generatedId is used without checking existence. A missing field silently writes id: undefined into templates.json, corrupting the gallery data.

Suggested Fix: Add: if (!validation.generatedId) { core.setFailed('Validation result missing generatedId'); return; }

description: "Comma-separated extra tags (e.g., ai, mongodb, kubernetes)"
placeholder: "ai, mongodb"
validations:
required: false
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Consider adding auto-merge opt-out for trusted publishers

There are cases where a trusted publisher submits a template but wants to merge the PR at a later date (e.g., for a private repo not yet public). Currently there's no way to opt out of auto-merge per submission.

Suggested Fix: Add a dropdown to the issue form with options like 'Yes (default)' and 'No - I'll merge manually', then conditionally skip the trusted-publisher label in the submission workflow when opt-out is selected.

@hemarina
Copy link
Copy Markdown
Contributor

hemarina commented Mar 27, 2026

@kristenwomack @haileyhuber8 Do we want to allow auto-approve? This is a design choice. I think the current blocker of PR submission is not how often it is reviewed. It's more about if internal contributors provide test proof of templates works, or they go to manual testing process (which the process is longer). I'm OK with auto approve but is a bit worry if the workflow is too automatic.

@hemarina hemarina requested a review from haileyhuber8 March 27, 2026 23:48
@jongio jongio marked this pull request as draft March 28, 2026 13:58
jongio and others added 2 commits March 28, 2026 15:57
… coverage

Review fixes (20 items from @hemarina + copilot-pull-request-reviewer):

Security (Critical):
- Prevent PR body forgery by verifying PR author is github-actions[bot]
  and branch matches template-submission-{N} pattern
- Add concurrency control to prevent lost-update race on templates.json
- Replace fixed EOF heredoc delimiter with dynamic one to prevent injection

Security (High):
- Wrap all JSON.parse/API calls in try/catch with descriptive errors
- Add timeout-minutes: 30 to gh pr checks --watch step
- Require HTTPS-only in inline validateUrl (was allowing HTTP)
- Document GITHUB_TOKEN limitation for cross-workflow triggers

Quality:
- Add URL canonicalization (strip www., .git, trailing slash, extra segments)
- Handle workflow_dispatch PRs gracefully in auto-merge
- Update action versions to match repo standard (checkout@v6, github-script@v8)
- Block admin-only tags (aicollection, popular) from automated submissions
- Fix bash -e compatibility in validation step
- Fix approval message to say issue submitter not PR author

Testing:
- Add 25 new tests for validate-template.js (URL canonicalization,
  SSRF protection, HTTPS enforcement, error cases)
- Total: 40/40 tests pass across 3 suites

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Validate enum fields (Author Type, IaC Provider) against allowlists
  to prevent textarea header-injection overrides
- Strip HTML from all free-text fields (title, description, author, tags)
  at both parse and update stages (defense-in-depth)
- Sanitize error output in issue comments to prevent markdown breakout
- Add field length limits (title: 200, description: 2000, author: 100)
  and tag limits (50 chars, max 20 per field) to prevent payload bloat
- Cross-validate branch number against issue number in auto-merge
  to prevent issue-reference mismatch attacks
- Strip HTML from individual CSV tag values

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jongio
Copy link
Copy Markdown
Member Author

jongio commented Mar 29, 2026

Addressing All Review Feedback

Thanks @hemarina for the thorough review! All 20 findings (yours + Copilot re-review) have been addressed across two commits:

Commit 2123344 — Review fixes (20 items)

Category Fixes
Security (Critical) PR body forgery prevention (bot author + branch pattern check), concurrency control on templates.json, dynamic heredoc delimiter
Security (High) try/catch on all JSON.parse/API calls, HTTPS-only in inline validateUrl, timeout on gh pr checks --watch, GITHUB_TOKEN limitation documented
Quality URL canonicalization (www/.git/trailing slash/extra segments), workflow_dispatch graceful handling, action versions updated (v6/v8), admin-only tag blocking, bash -e fix, approval message corrected
Testing 25 new tests for validate-template.js (40 total, 0 failures)

Commit 514f2ed — Input validation hardening

Fix Detail
Enum validation Author Type and IaC Provider validated against allowlists (prevents textarea header-injection)
HTML stripping stripHtml() applied to title, description, author, and tags at both parse and update stages
Markdown sanitization Error output sanitized before embedding in issue comments
Field length limits title: 200, description: 2000, author: 100 chars; tags: 50 chars, max 20 per field
Branch↔Issue cross-validation Auto-merge verifies branch number matches issue number in PR body

Re: auto-approve concern

The auto-merge pipeline now has 4 defense layers:

  1. PR authorship: Must be github-actions[bot] with branch template-submission-{N}
  2. Branch↔Issue cross-validation: Branch number must match referenced issue number
  3. Issue submitter verification: Original issue creator must be in trusted-publishers.json
  4. GITHUB_TOKEN gate: Label events from GITHUB_TOKEN don't trigger workflows — a maintainer must manually re-apply the label to activate auto-merge

In practice, auto-merge is human-gated (layer 4) and everything in templates.json is rollbackable.

Known architectural limitations (documented, not code-fixable)

  • Action version tags are floating (SHA pinning recommended for peter-evans/create-pull-request)
  • No rate limiting on submissions beyond GitHub defaults + concurrency serialization

The auto-merge design was over-engineered:
- GITHUB_TOKEN events can't trigger other workflows, making auto-merge dead code
- All security hardening (bot author check, branch cross-validation, trusted-publishers.json)
  existed solely to defend automated approval
- A human still had to manually re-apply the label anyway
- The extension submission pipeline has zero auto-merge and works fine

Simplified approach: issue -> validate -> PR -> human approves. Done.

Removed:
- .github/workflows/template-auto-merge.yml (122 lines)
- .github/trusted-publishers.json
- Trusted publisher check step from template-submission.yml
- GITHUB_TOKEN limitation comments and trusted-publisher label logic

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jongio
Copy link
Copy Markdown
Member Author

jongio commented Mar 29, 2026

Closing in favor of a fresh, simplified implementation. The auto-merge/trusted-publisher complexity was over-engineered — a human approval click is all that's needed. New PR incoming with clean design.

@jongio jongio closed this Mar 29, 2026
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.

Automate template submissions for trusted publishers (mirror extension pipeline)

3 participants