-
Notifications
You must be signed in to change notification settings - Fork 160
feat: integrate azd extensions into awesome-azd gallery #749
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
097aba1
feat: integrate azd extensions into awesome-azd gallery
jongio 92026b6
docs: add screenshots for PR review
jongio a9d76e7
fix: design review — a11y, dark mode, dead code, type safety
jongio 246b430
fix: address PR review feedback
jongio e054202
fix: contextual sidebar filters + remove screenshots
jongio 06d7539
feat: add daily workflow to sync extension versions
jongio be3a35e
fix: sync script handles {extensions:[]} registry format
jongio c3ba7af
fix: clarify website field applies to all extensions in registry
jongio 0505c7e
screenshots for PR #749
jongio 850d03c
fix: address PR review feedback (round 5)
jongio b619460
fix: address additional review feedback (round 5b)
jongio 808db66
feat: add author filter for extensions view
jongio 544e96b
fix: address PR review feedback (round 6)
jongio 4d85d8c
fix: MQ audit - TDZ crash, SSRF protection, fetch timeouts, a11y
jongio 2ac1916
fix: design review - keyboard a11y, CLS, redundant role
jongio 0d31627
fix: add missing website URLs for exec and rest extensions
jongio File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| name: "🧩 Submit an azd Extension" | ||
| description: "Add your azd extension to the Awesome azd gallery" | ||
| title: "[Extension]: " | ||
| labels: ["extension-submission"] | ||
| body: | ||
| - type: markdown | ||
| attributes: | ||
| value: | | ||
| Thanks for submitting your azd extension! Please fill out the information below. | ||
| Your extension's registry.json will be fetched and validated automatically. | ||
|
|
||
| - type: input | ||
| id: registry-url | ||
| attributes: | ||
| label: "Registry URL" | ||
| description: "URL to your extension's registry.json file (raw GitHub URL recommended)" | ||
| placeholder: "https://raw.githubusercontent.com/org/repo/main/registry.json" | ||
| validations: | ||
| required: true | ||
|
|
||
| - type: input | ||
| id: source-repo | ||
| attributes: | ||
| label: "Source Repository" | ||
| description: "GitHub repository URL for your extension" | ||
| placeholder: "https://github.com/org/repo" | ||
| validations: | ||
| required: true | ||
|
|
||
| - type: input | ||
| id: author | ||
| attributes: | ||
| label: "Author" | ||
| description: "Your name or organization" | ||
| validations: | ||
| required: true | ||
|
|
||
| - type: input | ||
| id: author-url | ||
| attributes: | ||
| label: "Author URL" | ||
| description: "URL to your GitHub profile or organization page" | ||
| placeholder: "https://github.com/username" | ||
| validations: | ||
| required: true | ||
|
|
||
| - type: dropdown | ||
| id: author-type | ||
| attributes: | ||
| label: "Author Type" | ||
| description: "Is this a Microsoft-authored or community extension?" | ||
| options: | ||
| - Community | ||
| - Microsoft | ||
| validations: | ||
| required: true | ||
|
|
||
| - type: input | ||
| id: website | ||
| attributes: | ||
| label: "Extension Website" | ||
| description: "URL to your extension's documentation website (optional). If your registry contains multiple extensions, this will be applied to all of them." | ||
| placeholder: "https://jongio.github.io/azd-app" | ||
| validations: | ||
| required: false | ||
|
|
||
| - type: textarea | ||
| id: additional-info | ||
| attributes: | ||
| label: "Additional Information" | ||
| description: "Any additional context about your extension (optional)" | ||
| validations: | ||
| required: false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,258 @@ | ||
| name: Extension Submission | ||
|
|
||
| on: | ||
| issues: | ||
| types: [labeled] | ||
| workflow_dispatch: | ||
| inputs: | ||
| registry_url: | ||
| description: "URL to the extension's registry.json" | ||
| required: true | ||
| source_repo: | ||
| description: "GitHub repository URL" | ||
| required: true | ||
| author: | ||
| description: "Author name" | ||
| required: true | ||
| author_url: | ||
| description: "Author GitHub URL" | ||
| required: true | ||
| author_type: | ||
| description: "Microsoft or Community" | ||
| required: true | ||
| default: "Community" | ||
| type: choice | ||
| options: | ||
| - Community | ||
| - Microsoft | ||
| website: | ||
| description: "Extension documentation website URL (optional)" | ||
| required: false | ||
|
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
| issues: write | ||
|
|
||
| jobs: | ||
| process-extension: | ||
| if: >- | ||
| github.event_name == 'workflow_dispatch' || | ||
| contains(github.event.issue.labels.*.name, 'extension-submission') | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: "20" | ||
|
|
||
| - name: Parse issue body | ||
| id: parse | ||
| uses: actions/github-script@v7 | ||
| env: | ||
| ISSUE_BODY: ${{ github.event.issue.body || '' }} | ||
| INPUT_REGISTRY_URL: ${{ inputs.registry_url }} | ||
| INPUT_SOURCE_REPO: ${{ inputs.source_repo }} | ||
| INPUT_AUTHOR: ${{ inputs.author }} | ||
| INPUT_AUTHOR_URL: ${{ inputs.author_url }} | ||
| INPUT_AUTHOR_TYPE: ${{ inputs.author_type }} | ||
| INPUT_WEBSITE: ${{ inputs.website }} | ||
| with: | ||
| script: | | ||
| if (context.eventName === 'workflow_dispatch') { | ||
| core.setOutput('registry_url', process.env.INPUT_REGISTRY_URL); | ||
| core.setOutput('source_repo', process.env.INPUT_SOURCE_REPO); | ||
| core.setOutput('author', process.env.INPUT_AUTHOR); | ||
| core.setOutput('author_url', process.env.INPUT_AUTHOR_URL); | ||
| core.setOutput('author_type', process.env.INPUT_AUTHOR_TYPE); | ||
| core.setOutput('website', process.env.INPUT_WEBSITE || ''); | ||
| return; | ||
| } | ||
|
|
||
| const body = process.env.ISSUE_BODY; | ||
|
|
||
| function extractField(body, fieldName) { | ||
| const regex = new RegExp(`### ${fieldName}\\s*\\n\\s*([^\\n]+)`, 'i'); | ||
| const match = body.match(regex); | ||
| return match ? match[1].trim() : ''; | ||
| } | ||
|
|
||
| const registryUrl = extractField(body, 'Registry URL'); | ||
| const sourceRepo = extractField(body, 'Source Repository'); | ||
| const author = extractField(body, 'Author'); | ||
| const authorUrl = extractField(body, 'Author URL'); | ||
| const authorType = extractField(body, 'Author Type'); | ||
| const website = extractField(body, 'Extension Website'); | ||
|
|
||
| if (!registryUrl || !sourceRepo || !author || !authorUrl) { | ||
| core.setFailed('Missing required fields in issue body'); | ||
| return; | ||
| } | ||
|
|
||
| core.setOutput('registry_url', registryUrl); | ||
| core.setOutput('source_repo', sourceRepo); | ||
| core.setOutput('author', author); | ||
| core.setOutput('author_url', authorUrl); | ||
| core.setOutput('author_type', authorType); | ||
| core.setOutput('website', website); | ||
|
|
||
| - name: Validate and fetch registry | ||
| id: validate | ||
| env: | ||
| REGISTRY_URL: ${{ steps.parse.outputs.registry_url }} | ||
| run: | | ||
| node website/scripts/validate-extension.js "$REGISTRY_URL" > 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 | ||
| echo 'EOF' | ||
| } >> $GITHUB_OUTPUT | ||
| fi | ||
|
|
||
| - name: Comment on validation failure | ||
| if: steps.validate.outputs.valid == 'false' && github.event_name != 'workflow_dispatch' | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.payload.issue.number, | ||
| body: `❌ **Extension validation failed**\n\nPlease check your registry.json and try again.\n\n\`\`\`\n${process.env.ERRORS}\n\`\`\`` | ||
| }); | ||
| env: | ||
| ERRORS: ${{ steps.validate.outputs.errors }} | ||
|
|
||
| - name: Update extensions.json | ||
| id: update | ||
| if: steps.validate.outputs.valid == 'true' | ||
| uses: actions/github-script@v7 | ||
| env: | ||
| VALIDATION_RESULT: ${{ steps.validate.outputs.result }} | ||
| EXT_AUTHOR: ${{ steps.parse.outputs.author }} | ||
| EXT_AUTHOR_URL: ${{ steps.parse.outputs.author_url }} | ||
| EXT_SOURCE_REPO: ${{ steps.parse.outputs.source_repo }} | ||
| EXT_REGISTRY_URL: ${{ steps.parse.outputs.registry_url }} | ||
| EXT_AUTHOR_TYPE: ${{ steps.parse.outputs.author_type }} | ||
| EXT_WEBSITE: ${{ steps.parse.outputs.website }} | ||
| with: | ||
| script: | | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
|
|
||
| function validateUrl(value, label) { | ||
| if (!value) return; | ||
| try { | ||
| const u = new URL(value); | ||
| if (!['http:', 'https:'].includes(u.protocol)) { | ||
| throw new Error(`unsafe protocol "${u.protocol}"`); | ||
| } | ||
| } catch (err) { | ||
| throw new Error(`Invalid ${label} URL "${value}": ${err.message}`); | ||
| } | ||
| } | ||
|
|
||
| const extensionsPath = path.join('website', 'static', 'extensions.json'); | ||
| const extensions = JSON.parse(fs.readFileSync(extensionsPath, 'utf8')); | ||
| const validatedExtensions = JSON.parse(process.env.VALIDATION_RESULT); | ||
|
|
||
| const author = process.env.EXT_AUTHOR; | ||
| const authorUrl = process.env.EXT_AUTHOR_URL; | ||
| const sourceRepo = process.env.EXT_SOURCE_REPO; | ||
| const registryUrl = process.env.EXT_REGISTRY_URL; | ||
| const authorType = process.env.EXT_AUTHOR_TYPE; | ||
| const website = process.env.EXT_WEBSITE; | ||
|
|
||
| // Validate all user-provided URLs | ||
| validateUrl(authorUrl, 'author'); | ||
| validateUrl(sourceRepo, 'source repo'); | ||
| validateUrl(website, 'website'); | ||
| validateUrl(registryUrl, 'registry'); | ||
|
|
||
| let added = []; | ||
| let skipped = []; | ||
|
|
||
| for (const ext of validatedExtensions) { | ||
| if (!ext.valid) { | ||
| skipped.push(`${ext.id} (validation errors)`); | ||
| continue; | ||
| } | ||
|
|
||
| const existingIndex = extensions.findIndex(e => e.id === ext.id); | ||
| const tags = authorType === 'Microsoft' ? ['msft', 'new'] : ['community', 'new']; | ||
|
|
||
| const entry = { | ||
| id: ext.id, | ||
| namespace: ext.namespace, | ||
| displayName: ext.displayName, | ||
| description: ext.description, | ||
| author: author, | ||
| authorUrl: authorUrl, | ||
| source: sourceRepo, | ||
| registryUrl: registryUrl, | ||
| latestVersion: ext.latestVersion, | ||
| capabilities: ext.capabilities, | ||
| platforms: ext.platforms, | ||
| tags: tags, | ||
| installCommand: `azd extension install ${ext.id}` | ||
| }; | ||
|
|
||
| if (website) { | ||
| entry.website = website; | ||
| } | ||
|
|
||
| if (existingIndex >= 0) { | ||
| extensions[existingIndex] = entry; | ||
| added.push(`${ext.id} (updated)`); | ||
| } else { | ||
| extensions.push(entry); | ||
| added.push(`${ext.id} (new)`); | ||
| } | ||
| } | ||
|
|
||
| fs.writeFileSync(extensionsPath, JSON.stringify(extensions, null, 2) + '\n'); | ||
|
|
||
| core.setOutput('added', added.join(', ')); | ||
| core.setOutput('skipped', skipped.join(', ')); | ||
|
|
||
| - name: Create Pull Request | ||
| if: steps.validate.outputs.valid == 'true' | ||
| uses: peter-evans/create-pull-request@v7 | ||
| with: | ||
| token: ${{ secrets.GITHUB_TOKEN }} | ||
| commit-message: "Add extension(s) from ${{ github.event_name == 'workflow_dispatch' && 'manual dispatch' || format('issue #{0}', github.event.issue.number) }}" | ||
| branch: "extension-submission-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.event.issue.number }}" | ||
| title: "Add extension(s) from ${{ github.event_name == 'workflow_dispatch' && 'manual dispatch' || format('#{0}', github.event.issue.number) }}" | ||
| body: | | ||
| This PR was automatically generated${{ github.event_name != 'workflow_dispatch' && format(' from issue #{0}', github.event.issue.number) || '' }}. | ||
|
|
||
| **Registry URL**: ${{ steps.parse.outputs.registry_url }} | ||
| **Extensions added**: ${{ steps.update.outputs.added || 'None' }} | ||
| **Extensions skipped**: ${{ steps.update.outputs.skipped || 'None' }} | ||
|
|
||
| Please review the changes to `website/static/extensions.json`. | ||
| labels: extension-submission | ||
|
|
||
| - name: Comment on success | ||
| if: steps.validate.outputs.valid == 'true' && github.event_name != 'workflow_dispatch' | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.payload.issue.number, | ||
| body: `✅ **Extension validated successfully!**\n\nA pull request has been created to add your extension to the gallery. It will be reviewed by the maintainers shortly.` | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| name: Sync Extension Versions | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: "0 6 * * *" # Daily at 6 AM UTC | ||
| workflow_dispatch: # Allow manual trigger | ||
|
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
|
|
||
| jobs: | ||
| sync-versions: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: "20" | ||
|
|
||
| - name: Sync extension versions | ||
| id: sync | ||
| run: | | ||
| OUTPUT=$(node website/scripts/sync-extension-versions.js 2>&1) | ||
| echo "$OUTPUT" | ||
| if git diff --quiet website/static/extensions.json; then | ||
| echo "changed=false" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "changed=true" >> $GITHUB_OUTPUT | ||
| { | ||
| echo 'summary<<EOF' | ||
| echo "$OUTPUT" | ||
| echo 'EOF' | ||
| } >> $GITHUB_OUTPUT | ||
| fi | ||
|
|
||
| - name: Create Pull Request | ||
| if: steps.sync.outputs.changed == 'true' | ||
| uses: peter-evans/create-pull-request@v7 | ||
| with: | ||
| commit-message: "chore: sync extension versions" | ||
| title: "chore: sync extension versions" | ||
| body: | | ||
| Automated daily sync of extension versions from their registries. | ||
|
|
||
| ${{ steps.sync.outputs.summary }} | ||
| branch: chore/sync-extension-versions | ||
| delete-branch: true | ||
| labels: automated |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| test-results/ | ||
| node_modules/ | ||
| *.png |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.