├── .devcontainer └── devcontainer.json ├── .github ├── ISSUE_TEMPLATES │ ├── bug.yml │ ├── config.yml │ ├── extension-unmaintained.yml │ ├── extension.yml │ └── other.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── publish-extensions.yml │ ├── publish-once.yml │ ├── sonar.yml │ └── validate-pr.yml ├── .gitignore ├── .gitpod.yml ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── .theia └── settings.json ├── .vscode └── launch.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── add-extension.js ├── bin └── publish-extensions ├── diff-extensions.js ├── docs ├── direct_publish_setup.md ├── exampleCI.yaml ├── extension_issues.md ├── external_contribution_request.md └── understanding_reports.md ├── extension-control ├── extensions.json ├── schema.json └── update.ts ├── extensions-schema.json ├── extensions.json ├── lib ├── constants.js ├── exec.js ├── helpers.ts ├── reportStat.js └── resolveExtension.js ├── package.json ├── publish-extension.js ├── publish-extensions.js ├── report-extensions.ts ├── sonar-project.properties ├── tsconfig.json └── types.d.ts /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "publish-extensions", 3 | "image": "mcr.microsoft.com/devcontainers/base", 4 | // Features to add to the dev container. More info: https://containers.dev/features. 5 | "features": { 6 | "ghcr.io/shyim/devcontainers-features/bun:0": {}, 7 | "ghcr.io/devcontainers/features/node:1": {}, 8 | "ghcr.io/devcontainers/features/python:1": {}, 9 | "ghcr.io/devcontainers/features/docker-in-docker:2": {}, 10 | "ghcr.io/devcontainers/features/java:1": {} 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report [publish-extensions] 2 | description: A bug report about a failure to publish via the open-vsx/publish-extensions repository 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: textarea 11 | id: describe 12 | attributes: 13 | label: Expected result and actual result 14 | description: What actually occurred, and what did you expect? 15 | value: "I expected extension X to publish successfully, but it is currently intermittently failing" 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: reproduce 20 | attributes: 21 | label: Please tell us what you did to get this error 22 | description: If you can provide a simple example, please also put it below! 23 | value: "1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error" 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Bug report [extension] 4 | url: https://github.com/open-vsx/publish-extensions/blob/HEAD/docs/extension_issues.md 5 | about: Please see this doc about reporting issues with extensions published from here 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/extension-unmaintained.yml: -------------------------------------------------------------------------------- 1 | name: Add an unmaintained extension 2 | description: I want an unmaintained extension to be published 3 | title: "[extension.id]: Publish to Open VSX [unmaintained]" 4 | labels: ["extension"] 5 | assignees: ["filiptronicek"] 6 | body: 7 | - type: checkboxes 8 | id: terms 9 | attributes: 10 | label: Pre-submission checklist 11 | description: We require new publication requests to be first communicated with the extension's maintainers (see [why](https://github.com/open-vsx/publish-extensions/blob/HEAD/README.md#when-to-add-an-extension)), it the author cannot publish the extension or is unresponsive, raise a PR. If you haven't made an issue for the extension owner yet, please create one in the extension's repository using [this template](https://github.com/open-vsx/publish-extensions/blob/HEAD/docs/external_contribution_request.md). 12 | options: 13 | - label: I have reached out to the extension maintainers to publish to Open VSX directly 14 | required: true 15 | - label: This extension has an [OSI-approved OSS license](https://opensource.org/licenses) 16 | required: true 17 | - type: textarea 18 | id: buildsteps 19 | attributes: 20 | label: How to build the extension? 21 | description: Please write down the commands and/or actions needed to produce a `.vsix` file for this extension. 22 | - type: textarea 23 | id: notes 24 | attributes: 25 | label: Anything else? 26 | description: If there are further details that could help with publishing the extension, put them here 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/extension.yml: -------------------------------------------------------------------------------- 1 | name: Add an extension 2 | description: I want an extension to be published via this repo 3 | title: "[extension.id]: Publish to Open VSX" 4 | labels: ["extension"] 5 | assignees: ["filiptronicek"] 6 | body: 7 | - type: checkboxes 8 | id: terms 9 | attributes: 10 | label: Pre-submission checklist 11 | description: We require new publication requests to be first communicated with the extension's maintainers (see [why](https://github.com/open-vsx/publish-extensions/blob/HEAD/README.md#when-to-add-an-extension)), it the author cannot publish the extension or is unresponsive, raise a PR. If you haven't made an issue for the extension owner yet, please create one in the extension's repository using [this template](https://github.com/open-vsx/publish-extensions/blob/HEAD/docs/external_contribution_request.md). 12 | options: 13 | - label: I have reached out to the extension maintainers to publish to Open VSX directly 14 | required: true 15 | - label: This extension has an [OSI-approved OSS license](https://opensource.org/licenses) 16 | required: true 17 | - type: textarea 18 | id: notes 19 | attributes: 20 | label: Anything else? 21 | description: If there are further details that could help with publishing the extension, put them here 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other 3 | about: Raise an issue about something not listed above 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 23 | 24 | - [ ] I have read the note above about PRs contributing or fixing extensions 25 | - [ ] I have tried reaching out to the extension maintainers about publishing this extension to Open VSX (if not, please create an issue in the extension's repo using [this template](https://github.com/open-vsx/publish-extensions/blob/HEAD/docs/external_contribution_request.md)). 26 | - [ ] This extension has an [OSI-approved OSS license](https://opensource.org/licenses) (we don't accept proprietary extensions in this repository) 27 | 28 | ## Description 29 | 30 | 31 | 32 | This PR introduces X change for Y reason. 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | commit-message: 8 | prefix: '⬆️ deps:' 9 | prefix-development: '⬆️ deps(dev):' 10 | rebase-strategy: auto 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | commit-message: 16 | prefix: '⬆️ deps(gha):' -------------------------------------------------------------------------------- /.github/workflows/publish-extensions.yml: -------------------------------------------------------------------------------- 1 | name: Publish extensions to open-vsx.org 2 | 3 | on: 4 | schedule: 5 | # Run at 03:03 UTC every day. 6 | - cron: "3 3 * * 1-5" 7 | workflow_dispatch: 8 | inputs: 9 | extensions: 10 | description: "Comma separated list of extensions to publish" 11 | required: false 12 | default: "" 13 | skipPublish: 14 | description: "Skip publishing to Open VSX, only build extensions" 15 | type: boolean 16 | required: false 17 | default: false 18 | forcefullyPublish: 19 | description: "Force publish to Open VSX, even if version is already published" 20 | type: boolean 21 | required: false 22 | default: false 23 | 24 | jobs: 25 | publish_extensions: 26 | env: 27 | EXTENSIONS: ${{ github.event.inputs.extensions }} 28 | SKIP_PUBLISH: ${{ github.event.inputs.skipPublish }} 29 | FORCE: ${{ github.event.inputs.forcefullyPublish }} 30 | name: Publish Extensions 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: actions/setup-node@v4.3.0 35 | with: 36 | node-version: "20.x" 37 | - uses: oven-sh/setup-bun@v2 38 | with: 39 | bun-version: latest 40 | - name: Set up pyenv 41 | uses: "gabrielfalcao/pyenv-action@32ef4d2c861170ce17ded56d10329d83f4c8f797" 42 | with: 43 | command: python --version 44 | - name: Set default global version 45 | run: | 46 | pyenv install 3.8 47 | pyenv global 3.8 48 | - uses: actions/setup-java@v4 49 | with: 50 | distribution: "microsoft" 51 | java-version: "21" 52 | - name: Install dependencies for native modules 53 | run: | 54 | sudo apt-get update 55 | sudo apt-get install libpango1.0-dev libgif-dev 56 | - run: npm install 57 | - run: npm i -g @vscode/vsce pnpm 58 | - run: node publish-extensions 59 | env: 60 | OVSX_PAT: ${{ secrets.OVSX_PAT }} 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | - name: Report results 63 | run: bun run ./report-extensions.ts 64 | - uses: actions/upload-artifact@v4 65 | if: always() 66 | with: 67 | name: report 68 | path: | 69 | /tmp/stat.json 70 | /tmp/result.md 71 | - uses: actions/upload-artifact@v4 72 | if: always() 73 | with: 74 | name: artifacts 75 | path: | 76 | /tmp/artifacts/*.vsix 77 | - name: Upload job summary 78 | if: always() 79 | run: cat /tmp/result.md >> $GITHUB_STEP_SUMMARY 80 | - name: Get previous job's status 81 | id: lastrun 82 | uses: filiptronicek/get-last-job-status@main 83 | - name: Slack Notification 84 | if: ${{ !github.event.inputs.extensions && ((success() && steps.lastrun.outputs.status == 'failed') || failure()) }} 85 | uses: rtCamp/action-slack-notify@v2 86 | env: 87 | SLACK_WEBHOOK: ${{ secrets.GITPOD_SLACK_WEBHOOK }} 88 | SLACK_COLOR: ${{ job.status }} 89 | check_parity: 90 | name: Check MS parity 91 | runs-on: ubuntu-latest 92 | needs: publish_extensions 93 | if: ${{ !github.event.inputs.extensions }} # only run on full runs 94 | steps: 95 | - uses: actions/checkout@v4 96 | - uses: actions/setup-node@v4.3.0 97 | with: 98 | node-version: "18.x" 99 | - run: npm install 100 | - name: Check parity of the top 4096 extensions 101 | run: node lib/reportStat.js 102 | -------------------------------------------------------------------------------- /.github/workflows/publish-once.yml: -------------------------------------------------------------------------------- 1 | name: Publish an extensions from a vsix file 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | extensions: 7 | description: "URL of the `.vsix`" 8 | required: true 9 | namespace: 10 | description: "If a namespace does not exist for the author, provide it here" 11 | required: false 12 | flags: 13 | description: "Additional flags to pass to the `vsce publish` command" 14 | required: false 15 | default: "" 16 | 17 | jobs: 18 | publish: 19 | name: node publish-extensions 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/setup-node@v4.3.0 23 | with: 24 | node-version: "20.x" 25 | - name: Download extension file 26 | run: wget -O extension.vsix "${{ github.event.inputs.extensions }}" 27 | - name: Create Open VSX namespace 28 | if: ${{ github.event.inputs.namespace }} 29 | run: npx ovsx create-namespace ${{ github.event.inputs.namespace }} -p ${{ secrets.OVSX_PAT }} 30 | - name: Publish to Open VSX 31 | run: npx ovsx publish extension.vsix -p ${{ secrets.OVSX_PAT }} ${{ github.event.inputs.flags }} 32 | -------------------------------------------------------------------------------- /.github/workflows/sonar.yml: -------------------------------------------------------------------------------- 1 | name: Sonar 2 | on: 3 | workflow_run: 4 | workflows: [Validate PR] 5 | types: [completed] 6 | jobs: 7 | sonar: 8 | name: Sonar 9 | runs-on: ubuntu-latest 10 | if: github.event.workflow_run.conclusion == 'success' 11 | steps: 12 | - name: Download PR number artifact 13 | if: github.event.workflow_run.event == 'pull_request' 14 | uses: dawidd6/action-download-artifact@v3 15 | with: 16 | workflow: Validate PR 17 | run_id: ${{ github.event.workflow_run.id }} 18 | name: PR_NUMBER 19 | - name: Read PR_NUMBER.txt 20 | if: github.event.workflow_run.event == 'pull_request' 21 | id: pr_number 22 | uses: juliangruber/read-file-action@v1 23 | with: 24 | path: ./PR_NUMBER.txt 25 | - name: Request GitHub API for PR data 26 | if: github.event.workflow_run.event == 'pull_request' 27 | uses: octokit/request-action@v2.x 28 | id: get_pr_data 29 | with: 30 | route: GET /repos/{full_name}/pulls/{number} 31 | number: ${{ steps.pr_number.outputs.content }} 32 | full_name: ${{ github.event.repository.full_name }} 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | - uses: actions/checkout@v4 36 | with: 37 | repository: ${{ github.event.workflow_run.head_repository.full_name }} 38 | ref: ${{ github.event.workflow_run.head_branch }} 39 | fetch-depth: 0 40 | - name: Checkout base branch 41 | if: github.event.workflow_run.event == 'pull_request' 42 | run: | 43 | git remote add upstream ${{ github.event.repository.clone_url }} 44 | git fetch upstream 45 | git checkout -B ${{ fromJson(steps.get_pr_data.outputs.data).base.ref }} upstream/${{ fromJson(steps.get_pr_data.outputs.data).base.ref }} 46 | git checkout ${{ github.event.workflow_run.head_branch }} 47 | git clean -ffdx && git reset --hard HEAD 48 | - name: SonarCloud Scan on PR 49 | if: github.event.workflow_run.event == 'pull_request' 50 | uses: sonarsource/sonarcloud-github-action@master 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 54 | with: 55 | args: > 56 | -Dsonar.scm.revision=${{ github.event.workflow_run.head_sha }} 57 | -Dsonar.pullrequest.key=${{ fromJson(steps.get_pr_data.outputs.data).number }} 58 | -Dsonar.pullrequest.branch=${{ fromJson(steps.get_pr_data.outputs.data).head.ref }} 59 | -Dsonar.pullrequest.base=${{ fromJson(steps.get_pr_data.outputs.data).base.ref }} 60 | - name: SonarCloud Scan on push 61 | if: github.event.workflow_run.event == 'push' && github.event.workflow_run.head_repository.full_name == github.event.repository.full_name 62 | uses: sonarsource/sonarcloud-github-action@master 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 66 | with: 67 | args: > 68 | -Dsonar.scm.revision=${{ github.event.workflow_run.head_sha }} 69 | -Dsonar.branch.name=${{ github.event.workflow_run.head_branch }} 70 | -------------------------------------------------------------------------------- /.github/workflows/validate-pr.yml: -------------------------------------------------------------------------------- 1 | name: Validate PR 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | publish_extensions: 9 | env: 10 | VALIDATE_PR: true 11 | SKIP_PUBLISH: true 12 | FORCE: true 13 | name: Validate PR 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4.3.0 18 | with: 19 | node-version: "18.x" 20 | - uses: oven-sh/setup-bun@v2 21 | with: 22 | bun-version: latest 23 | - run: npm install 24 | - run: npm i -g @vscode/vsce pnpm 25 | - name: Set up pyenv 26 | uses: "gabrielfalcao/pyenv-action@32ef4d2c861170ce17ded56d10329d83f4c8f797" 27 | with: 28 | command: python --version 29 | - name: Set default global version 30 | run: | 31 | pyenv install 3.8 32 | pyenv global 3.8 33 | - run: EXTENSIONS=$(node diff-extensions) node publish-extensions 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | - name: Report results 37 | run: bun run ./report-extensions.ts 38 | - uses: actions/upload-artifact@v4 39 | if: always() 40 | with: 41 | name: report 42 | path: | 43 | /tmp/stat.json 44 | /tmp/result.md 45 | - name: Upload job summary 46 | run: cat /tmp/result.md >> $GITHUB_STEP_SUMMARY 47 | - name: Save PR number to file 48 | if: github.event_name == 'pull_request' 49 | run: echo ${{ github.event.number }} > PR_NUMBER.txt 50 | - name: Archive PR number 51 | if: github.event_name == 'pull_request' 52 | uses: actions/upload-artifact@v4 53 | with: 54 | name: PR_NUMBER 55 | path: PR_NUMBER.txt 56 | 57 | 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | extensions.json.old 4 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: | 3 | npm i -g @vscode/vsce pnpm 4 | curl -fsSL https://bun.sh/install | bash 5 | npm i 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.17.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | /extensions.json 3 | 4 | *.yml 5 | *.yaml 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "endOfLine": "auto", 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.theia/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "terminal.integrated.scrollback": 20000 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": ["/**"], 12 | "program": "${file}", 13 | "outFiles": ["${workspaceFolder}/**/*.js"] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Code of Conduct 2 | 3 | **Version 2.0 4 | January 1, 2023** 5 | 6 | ## Our Pledge 7 | 8 | In the interest of fostering an open and welcoming environment, we as community members, contributors, Committers[^1], and Project Leads (collectively "Contributors") pledge to make participation in our projects and our community a harassment-free and inclusive experience for everyone. 9 | 10 | This Community Code of Conduct ("Code") outlines our behavior expectations as members of our community in all Eclipse Foundation activities, both offline and online. It is not intended to govern scenarios or behaviors outside of the scope of Eclipse Foundation activities. Nor is it intended to replace or supersede the protections offered to all our community members under the law. Please follow both the spirit and letter of this Code and encourage other Contributors to follow these principles into our work. Failure to read or acknowledge this Code does not excuse a Contributor from compliance with the Code. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contribute to creating a positive and professional environment include: 15 | 16 | - Using welcoming and inclusive language; 17 | - Actively encouraging all voices; 18 | - Helping others bring their perspectives and listening actively. If you find yourself dominating a discussion, it is especially important to encourage other voices to join in; 19 | - Being respectful of differing viewpoints and experiences; 20 | - Gracefully accepting constructive criticism; 21 | - Focusing on what is best for the community; 22 | - Showing empathy towards other community members; 23 | - Being direct but professional; and 24 | - Leading by example by holding yourself and others accountable 25 | 26 | Examples of unacceptable behavior by Contributors include: 27 | 28 | - The use of sexualized language or imagery; 29 | - Unwelcome sexual attention or advances; 30 | - Trolling, insulting/derogatory comments, and personal or political attacks; 31 | - Public or private harassment, repeated harassment; 32 | - Publishing others' private information, such as a physical or electronic address, without explicit permission; 33 | - Violent threats or language directed against another person; 34 | - Sexist, racist, or otherwise discriminatory jokes and language; 35 | - Posting sexually explicit or violent material; 36 | - Sharing private content, such as emails sent privately or non-publicly, or unlogged forums such as IRC channel history; 37 | - Personal insults, especially those using racist or sexist terms; 38 | - Excessive or unnecessary profanity; 39 | - Advocating for, or encouraging, any of the above behavior; and 40 | - Other conduct which could reasonably be considered inappropriate in a professional setting 41 | 42 | ## Our Responsibilities 43 | 44 | With the support of the Eclipse Foundation employees, consultants, officers, and directors (collectively, the "Staff"), Committers, and Project Leads, the Eclipse Foundation Conduct Committee (the "Conduct Committee") is responsible for clarifying the standards of acceptable behavior. The Conduct Committee takes appropriate and fair corrective action in response to any instances of unacceptable behavior. 45 | 46 | ## Scope 47 | 48 | This Code applies within all Project, Working Group, and Interest Group spaces and communication channels of the Eclipse Foundation (collectively, "Eclipse spaces"), within any Eclipse-organized event or meeting, and in public spaces when an individual is representing an Eclipse Foundation Project, Working Group, Interest Group, or their communities. Examples of representing a Project or community include posting via an official social media account, personal accounts, or acting as an appointed representative at an online or offline event. Representation of Projects, Working Groups, and Interest Groups may be further defined and clarified by Committers, Project Leads, or the Staff. 49 | 50 | ## Enforcement 51 | 52 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the Conduct Committee via conduct@eclipse-foundation.org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Without the explicit consent of the reporter, the Conduct Committee is obligated to maintain confidentiality with regard to the reporter of an incident. The Conduct Committee is further obligated to ensure that the respondent is provided with sufficient information about the complaint to reply. If such details cannot be provided while maintaining confidentiality, the Conduct Committee will take the respondent‘s inability to provide a defense into account in its deliberations and decisions. Further details of enforcement guidelines may be posted separately. 53 | 54 | Staff, Committers and Project Leads have the right to report, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code, or to block temporarily or permanently any Contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. Any such actions will be reported to the Conduct Committee for transparency and record keeping. 55 | 56 | Any Staff (including officers and directors of the Eclipse Foundation), Committers, Project Leads, or Conduct Committee members who are the subject of a complaint to the Conduct Committee will be recused from the process of resolving any such complaint. 57 | 58 | ## Responsibility 59 | 60 | The responsibility for administering this Code rests with the Conduct Committee, with oversight by the Executive Director and the Board of Directors. For additional information on the Conduct Committee and its process, please write to . 61 | 62 | ## Investigation of Potential Code Violations 63 | 64 | All conflict is not bad as a healthy debate may sometimes be necessary to push us to do our best. It is, however, unacceptable to be disrespectful or offensive, or violate this Code. If you see someone engaging in objectionable behavior violating this Code, we encourage you to address the behavior directly with those involved. If for some reason, you are unable to resolve the matter or feel uncomfortable doing so, or if the behavior is threatening or harassing, please report it following the procedure laid out below. 65 | 66 | Reports should be directed to . It is the Conduct Committee’s role to receive and address reported violations of this Code and to ensure a fair and speedy resolution. 67 | 68 | The Eclipse Foundation takes all reports of potential Code violations seriously and is committed to confidentiality and a full investigation of all allegations. The identity of the reporter will be omitted from the details of the report supplied to the accused. Contributors who are being investigated for a potential Code violation will have an opportunity to be heard prior to any final determination. Those found to have violated the Code can seek reconsideration of the violation and disciplinary action decisions. Every effort will be made to have all matters disposed of within 60 days of the receipt of the complaint. 69 | 70 | ## Actions 71 | 72 | Contributors who do not follow this Code in good faith may face temporary or permanent repercussions as determined by the Conduct Committee. 73 | 74 | This Code does not address all conduct. It works in conjunction with our [Communication Channel Guidelines](https://www.eclipse.org/org/documents/communication-channel-guidelines/), [Social Media Guidelines](https://www.eclipse.org/org/documents/social_media_guidelines.php), [Bylaws](https://www.eclipse.org/org/documents/eclipse-foundation-be-bylaws-en.pdf), and [Internal Rules](https://www.eclipse.org/org/documents/ef-be-internal-rules.pdf) which set out additional protections for, and obligations of, all contributors. The Foundation has additional policies that provide further guidance on other matters. 75 | 76 | It’s impossible to spell out every possible scenario that might be deemed a violation of this Code. Instead, we rely on one another’s good judgment to uphold a high standard of integrity within all Eclipse Spaces. Sometimes, identifying the right thing to do isn’t an easy call. In such a scenario, raise the issue as early as possible. 77 | 78 | ## No Retaliation 79 | 80 | The Eclipse community relies upon and values the help of Contributors who identify potential problems that may need to be addressed within an Eclipse Space. Any retaliation against a Contributor who raises an issue honestly is a violation of this Code. That a Contributor has raised a concern honestly or participated in an investigation, cannot be the basis for any adverse action, including threats, harassment, or discrimination. If you work with someone who has raised a concern or provided information in an investigation, you should continue to treat the person with courtesy and respect. If you believe someone has retaliated against you, report the matter as described by this Code. Honest reporting does not mean that you have to be right when you raise a concern; you just have to believe that the information you are providing is accurate. 81 | 82 | False reporting, especially when intended to retaliate or exclude, is itself a violation of this Code and will not be accepted or tolerated. 83 | 84 | Everyone is encouraged to ask questions about this Code. Your feedback is welcome, and you will get a response within three business days. Write to . 85 | 86 | ## Amendments 87 | 88 | The Eclipse Foundation Board of Directors may amend this Code from time to time and may vary the procedures it sets out where appropriate in a particular case. 89 | 90 | ### Attribution 91 | 92 | This Code was inspired by the [Contributor Covenant](https://www.contributor-covenant.org/), version 1.4, available [here](https://www.contributor-covenant.org/version/1/4/code-of-conduct/). 93 | 94 | [^1]: Capitalized terms used herein without definition shall have the meanings assigned to them in the Bylaws. 95 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Eclipse Open VSX Publish Extensions 2 | 3 | Thanks for stopping by and willing to contribute to `publish-extensions`! Below are some general rules to abide by when contributing to the repository. 4 | 5 | ## Code of Conduct 6 | 7 | This project is governed by the [Eclipse Community Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. 8 | 9 | ## Communication 10 | 11 | The following communication channels are available: 12 | 13 | - [publish-extensions issues](https://github.com/open-vsx/publish-extensions/issues) - for general issues (bug reports, feature requests, etc.) 14 | - [open-vsx.org issues](https://github.com/EclipseFdn/open-vsx.org/issues) - for issues related to [open-vsx.org](https://open-vsx.org/) (outage reports, requests about extensions and namespaces, etc.) 15 | - [Developer mailing list](https://accounts.eclipse.org/mailing-list/openvsx-dev) - for organizational issues (e.g. elections of new committers) 16 | 17 | ## How to Contribute 18 | 19 | Before your pull request can be accepted, you must electronically sign the [Eclipse Contributor Agreement](https://www.eclipse.org/legal/ECA.php). 20 | 21 | Unless you are an elected committer of this project, you must include a `Signed-off-by` line in the commit message. This line can be generated with the [-s flag of git commit](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s). By doing this you confirm that your contribution conforms to the Eclipse Contributor Agreement. 22 | 23 | For more information, see the [Eclipse Foundation Project Handbook](https://www.eclipse.org/projects/handbook/#resources-commit). 24 | 25 | ## When to Add an Extension? 26 | 27 | A goal of Open VSX is to have extension maintainers publish their extensions [according to the documentation](https://github.com/eclipse/openvsx/wiki/Publishing-Extensions)[^guide]. The first step we recommend is to open an issue with the extension owner[^issue]. If the extension owner is unresponsive for some time, this repo (`publish-extensions`) can be used **as a temporary workaround** to ensure the extension is published to Open VSX. 28 | 29 | In the long-run it is better for extension owners to publish their own extensions because: 30 | 31 | 1. Any future issues (features/bugs) with any published extensions in Open VSX will be directed to their original repo/source-control, and not confused with this repo `publish-extensions`. 32 | 2. Extensions published by official authors are shown within the Open VSX marketplace as such. Whereas extensions published via `publish-extensions` as Published by 33 | open-vsx. 34 | 3. Extension owners who publish their own extensions get greater flexibility on the publishing/release process, therefore ensure more accuracy/stability. For instance, in some cases `publish-extensions` has build steps within this repository, which can cause some uploaded extension versions to break (e.g. if an extensions's build step changes). 35 | 36 | > **Warning**: We only accept extensions with an [OSI-approved open source license](https://opensource.org/licenses) here. If you want to have an extension with a proprietary or non-approved license published, please ask its maintainers to publish it[^proprietary]. 37 | 38 | Now that you know whether you should contribute, let's learn how to contribute! Read on in [DEVELOPMENT.md](DEVELOPMENT.md). 39 | 40 | ## Other contributions 41 | 42 | Contributions involving docs, publishing processes and others should be first discussed in an issue. This is not necessary for small changes like typo fixes. 43 | 44 | [^proprietary]: Proprietary extensions are allowed on https://open-vsx.org, but cannot be published through this repository 45 | [^guide]: We have our own guide for extension authors with the whole publishing process: [direct_publish_setup.md](docs/direct_publish_setup.md) 46 | [^issue]: We have a document that can be used as a template for these kinds of issues: [external_contribution_request.md](docs/external_contribution_request.md) 47 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | ## How to Add an Extension? 2 | 3 | ### Prerequisites 4 | 5 | - [Node.js](https://nodejs.org/en/) (we use Node 18) 6 | - Ubuntu Linux (Windows and macOS work fine for adding extensions, but extensions are always tested on Ubuntu, so they only need to build correctly there) 7 | 8 | To add an extension to this repo, clone the repo[^clone], install the dependencies[^deps], and use the following command: 9 | 10 | ```bash 11 | node add-extension.js ext.id https://github.com/x/y --optional arg 12 | ``` 13 | 14 | Or, if the extension you want to add exists on the MS Marketplace[^ms], you can simply feed the script the item URL (this automatically populates both the ID and git repository). 15 | 16 | ```bash 17 | node add-extension.js https://marketplace.visualstudio.com/items?itemName=ext.id --optional arg 18 | ``` 19 | 20 | All of the arguments are also valid options if you add the extension manually to the JSON file directly. You can find them in the [extension-schema.json file](https://github.com/open-vsx/publish-extensions/blob/HEAD/extensions-schema.json). 21 | 22 | See [Publishing options](#publishing-options) below for a quick guide. 23 | 24 | ⚠️ Some extensions require additional build steps, and failing to execute them may lead to a broken extension published to Open VSX. Please check the extension's `scripts` section in the package.json file to find such steps; usually they are named `build` or similar. In case the build steps are included in the [vscode:prepublish](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#prepublish-step) script, they are executed automatically, so it's not necessary to mention them explicitly. Otherwise, please include them in the `prepublish` value, e.g. `"prepublish": "npm run build"`. 25 | 26 | Click the button below to start a [Gitpod](https://gitpod.io) workspace where you can run the scripts contained in this repository: 27 | 28 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/open-vsx/publish-extensions) 29 | 30 | ## Publishing Options 31 | 32 | The best way to add an extension here is to [open this repository in Gitpod](https://gitpod.io/#https://github.com/open-vsx/publish-extensions) and [add a new entry to `extensions.json`](#how-to-add-an-extension). 33 | 34 | To test, run: 35 | 36 | ``` 37 | GITHUB_TOKEN=your_pat EXTENSIONS=rebornix.ruby SKIP_PUBLISH=true node publish-extensions.js 38 | ``` 39 | 40 | ### `GITHUB_TOKEN` 41 | 42 | For testing locally, we advise you to provide a [GitHub Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) for release and file resolution in our scripts. Otherwise, publishing can work in our workflow but fail for you locally and vice-a-versa. 43 | 44 | You can create one in your [GitHub Token Settings](https://github.com/settings/tokens). This token does not require any special permissions. 45 | 46 | ```jsonc 47 | // Unique Open VSX extension ID in the form "." 48 | "rebornix.ruby": { 49 | // Repository URL to clone and publish from. If the extension publishes `.vsix` files as release artifacts, this will determine the repo to fetch the releases from. 50 | "repository": "https://github.com/redhat-developer/vscode-yaml" 51 | }, 52 | ``` 53 | 54 | ## How do extensions get updated? 55 | 56 | The publishing job auto infers the latest version published to the MS Marketplace[^ms] using [`vsce`](https://www.npmjs.com/package/vsce) and then tries to resolve a `vsix` file using a [GitHub Release asset](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases) or, when one doesn't exist, it tries to find a commit to a build associated with the version using tags and commits around the last MS Marketplace[^ms] updated date. 57 | 58 | ## How are Extensions Published? 59 | 60 | Every night (Monday-Friday) at [03:03 UTC](https://github.com/open-vsx/publish-extensions/blob/a95d871811e490e1d24fd233b4047cac03f293a2/.github/workflows/publish-extensions.yml#L6), a [GitHub Actions workflow](https://github.com/open-vsx/publish-extensions/blob/a95d871811e490e1d24fd233b4047cac03f293a2/.github/workflows/publish-extensions.yml#L25-L68) goes through all entries in [`extensions.json`](./extensions.json), and checks for every entry whether it needs to be published to https://open-vsx.org or not (whether it is up-to-date). 61 | 62 | The [publishing process](https://github.com/open-vsx/publish-extensions/blob/master/publish-extension.js) can be summarized like this: 63 | 64 | 1. [`git clone "repository"`](https://github.com/open-vsx/publish-extensions/blob/a0fa4378a6621fb4d660a3bc7cefe71e074c077f/lib/resolveExtension.js#L53) 65 | 66 | If a `custom` property is provided, then every command from the array is executed. Otherwise, the following 2 steps are executed: (steps 4 and 5 are executed in both cases) 67 | 68 | 2. [`npm install`](https://github.com/open-vsx/publish-extensions/blob/a0fa4378a6621fb4d660a3bc7cefe71e074c077f/publish-extension.js#L56) (or `yarn install` if a `yarn.lock` file is detected in the repository) 69 | 3. _([`"prepublish"`](https://github.com/open-vsx/publish-extensions/blob/fcf903b3a3d7df1c7f7bc7ce20f21b8a9d49e5d4/publish-extension.js#L79))_ 70 | 4. _([`ovsx create-namespace "publisher"`](https://github.com/open-vsx/publish-extensions/blob/fcf903b3a3d7df1c7f7bc7ce20f21b8a9d49e5d4/publish-extension.js#L135-L140) if it doesn't already exist)_ 71 | 5. [`ovsx publish`](https://github.com/open-vsx/publish-extensions/blob/fcf903b3a3d7df1c7f7bc7ce20f21b8a9d49e5d4/publish-extension.js#L142) (with `--yarn` if a `yarn.lock` file was detected earlier) 72 | 73 | See all `ovsx` CLI options [here](https://github.com/eclipse/openvsx/blob/master/cli/README.md). 74 | 75 | ## Environment Variables 76 | 77 | Custom commands such as `prepublish` and the ones inside the `custom`-array receive a few environment variables 78 | in order to perform advanced tasks such as executing operations based on the extension version. 79 | 80 | Following environment variables are available: 81 | 82 | - `EXTENSION_ID`: the extension ID, e.g. `rebornix.ruby` 83 | - `EXTENSION_PUBLISHER`: the extension publisher, e.g. `rebornix` 84 | - `EXTENSION_NAME`: the extension name, e.g. `ruby` 85 | - `VERSION`: the version of the extension to publish, e.g. `0.1.0` 86 | - `MS_VERSION`: the latest version of the extension on the MS Marketplace[^ms], e.g. `0.1.0` 87 | - `OVSX_VERSION`: the latest version of the extension on Open VSX, e.g. `0.1.0` 88 | 89 | [publish-extensions-job]: https://github.com/open-vsx/publish-extensions/blob/master/.github/workflows/publish-extensions.yml 90 | 91 | [^ms]: [The Microsoft Visual Studio Code Extensions Marketplace](https://marketplace.visualstudio.com/) 92 | [^clone]: `git clone https://github.com/open-vsx/publish-extensions/` 93 | [^deps]: `npm i` 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Publish and Deprecate Extensions at Open VSX 2 | 3 | [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/open-vsx/publish-extensions) 4 | 5 | This repo contains: 6 | 7 | - A CI script for publishing open-source VS Code extensions to [open-vsx.org](https://open-vsx.org). 8 | - [`extensions.json`](https://github.com/open-vsx/publish-extensions/blob/master/extensions.json) file specifying which extensions get auto-published to [open-vsx.org](https://open-vsx.org). 9 | - [`extension-control/extensions.json`](https://github.com/open-vsx/publish-extensions/tree/master/extension-control) file indicating malicious and deprecated extensions. 10 | 11 | For instructions on auto-publishing extensions, marking extensions as deprecated, and flagging malicious extensions, please see the [Open VSX Wiki](https://github.com/EclipseFdn/open-vsx.org/wiki). 12 | 13 | This repository is open for contributions, but before you contribute, please read our [Contribution guidelines](CONTRIBUTING.md). 14 | -------------------------------------------------------------------------------- /add-extension.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2022 Gitpod and others 3 | * 4 | * This program and the accompanying materials are made available under the 5 | * terms of the Eclipse Public License v. 2.0 which is available at 6 | * http://www.eclipse.org/legal/epl-2.0. 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | ********************************************************************************/ 10 | 11 | // 12 | // Usage: 13 | // node add-extension.js ext.id https://github.com/my-org/repo 14 | // Optional extra arguments [see: extensions-schema.json]: 15 | // node add-extension.js abusaidm.html-snippets2 https://github.com/my-org/repo --location 'packages/xy' 16 | // 17 | 18 | // @ts-check 19 | const fs = require("fs"); 20 | const minimist = require("minimist"); 21 | const util = require("util"); 22 | const exec = require("./lib/exec"); 23 | const extensionsSchema = require("./extensions-schema.json"); 24 | const fetch = require("node-fetch"); 25 | const { getPublicGalleryAPI } = require("@vscode/vsce/out/util"); 26 | const parseXmlManifest = require("@vscode/vsce/out/xml").parseXmlManifest; 27 | const { ExtensionQueryFlags, PublishedExtension } = require("azure-devops-node-api/interfaces/GalleryInterfaces"); 28 | 29 | const flags = [ 30 | ExtensionQueryFlags.IncludeMetadata, 31 | ExtensionQueryFlags.IncludeAssetUri, 32 | ExtensionQueryFlags.IncludeFiles, 33 | ExtensionQueryFlags.IncludeLatestVersionOnly, 34 | ]; 35 | 36 | const msGalleryApi = getPublicGalleryAPI(); 37 | msGalleryApi.client["_allowRetries"] = true; 38 | msGalleryApi.client["_maxRetries"] = 5; 39 | 40 | const getRepositoryFromMarketplace = async (/** @type {string} */ id) => { 41 | /** @type {[PromiseSettledResult]} */ 42 | let [msExtension] = await Promise.allSettled([msGalleryApi.getExtension(id, flags)]); 43 | if (msExtension.status === "fulfilled") { 44 | const vsixManifest = 45 | msExtension.value?.versions && 46 | msExtension.value?.versions[0].files?.find( 47 | (file) => file.assetType === "Microsoft.VisualStudio.Services.VsixManifest", 48 | )?.source; 49 | const response = await fetch(vsixManifest); 50 | const data = await parseXmlManifest(await response.text()); 51 | const url = new URL( 52 | data.PackageManifest.Metadata[0].Properties[0].Property.find( 53 | (property) => property.$.Id === "Microsoft.VisualStudio.Services.Links.Source", 54 | ).$.Value, 55 | ); 56 | 57 | if (url.host === "github.com") { 58 | return url.toString().replace(".git", ""); 59 | } 60 | 61 | return url.toString(); 62 | } 63 | }; 64 | 65 | (async () => { 66 | // Parse args 67 | const argv = minimist(process.argv.slice(2)); // without executable & script path 68 | 69 | // Check positional args 70 | if (argv._.length === 0) { 71 | console.error("Need two positional arguments: ext-id, repo-url or a Microsoft Marketplace URL"); 72 | process.exit(1); 73 | } 74 | 75 | let [extID, repoURL] = argv._; 76 | try { 77 | const urlObject = new URL(extID); 78 | if (urlObject.host === "marketplace.visualstudio.com") { 79 | const id = urlObject.searchParams.get("itemName"); 80 | if (!id) { 81 | console.error(`Couldn\'t get the extension ID from ${extID}`); 82 | process.exit(1); 83 | } else { 84 | extID = id; 85 | const url = await getRepositoryFromMarketplace(id); 86 | if (!url) { 87 | console.error(`Couldn\'t get the repository URL for ${extID}`); 88 | process.exit(1); 89 | } else { 90 | repoURL = url; 91 | } 92 | } 93 | } 94 | } catch { 95 | } finally { 96 | if (argv._.length < 2 && !repoURL) { 97 | console.error( 98 | "Need two positional arguments: ext-id, repo-url, since the provided argument is not a Marketplace URL", 99 | ); 100 | process.exit(1); 101 | } 102 | } 103 | 104 | const extDefinition = { 105 | repository: repoURL, 106 | }; 107 | 108 | // Validate extra args 109 | delete argv._; // delete positional arguments 110 | for (const arg of Object.keys(argv)) { 111 | const propDef = extensionsSchema.additionalProperties.properties[arg]; 112 | // console.debug(`arg=${arg}:`, argv[arg], propDef) 113 | if (!propDef) { 114 | console.error(`argument '${arg}' not found in ./extensions-schema.json`); 115 | process.exit(1); 116 | } 117 | 118 | // parse & validate value 119 | if (propDef.type === "string") { 120 | extDefinition[arg] = String(argv[arg]); // minimist might've assumed a different type (e.g. number) 121 | } else if (propDef.type === "number") { 122 | if (typeof argv[arg] !== "number") { 123 | console.error( 124 | `argument '${arg}' should be type '${propDef.type}' but yours seems to be '${typeof argv[arg]}'`, 125 | ); 126 | process.exit(1); 127 | } 128 | extDefinition[arg] = argv[arg]; // numbers are parsed by minimist already 129 | } else { 130 | console.error( 131 | `argument '${arg}' is of type '${propDef.type}' which is not implemented by this script, sorry`, 132 | ); 133 | process.exit(1); 134 | } 135 | } 136 | console.info("Adding extension:", util.inspect(extDefinition, { colors: true, compact: false })); 137 | 138 | // Read current file 139 | const extensions = Object.entries( 140 | JSON.parse(await fs.promises.readFile("./extensions.json", { encoding: "utf8" })), 141 | ); 142 | // Sort extensions (most are, but not always) 143 | extensions.sort(([k1], [k2]) => k1.localeCompare(k2)); 144 | 145 | const originalList = JSON.stringify(Object.fromEntries(extensions), undefined, 2); 146 | 147 | // Find position & insert extension 148 | for (let i = 0; i < extensions.length; i++) { 149 | const [currentID] = extensions[i]; 150 | // console.debug(i, currentID) 151 | const diff = currentID.localeCompare(extID, undefined, { sensitivity: "base" }); 152 | if (diff === 0) { 153 | console.error("Extension already defined:", currentID); 154 | process.exit(1); 155 | } 156 | if (diff > 0) { 157 | extensions.splice(i, 0, [extID, extDefinition]); 158 | break; 159 | } 160 | } 161 | 162 | // Persist changes 163 | await fs.promises.writeFile( 164 | "./extensions.json", 165 | JSON.stringify(Object.fromEntries(extensions), undefined, 2) + "\n", // add newline at EOF 166 | { encoding: "utf8" }, 167 | ); 168 | 169 | console.info(`Successfully added ${extID}`); 170 | if (process.env.TEST_EXTENSION === "false") { 171 | console.info("Skipping tests, TEST_EXTENSION was provided."); 172 | process.exit(0); 173 | } 174 | 175 | console.info(`Trying to build ${extID} for the first time`); 176 | 177 | process.env.EXTENSIONS = extID; 178 | process.env.FORCE = "true"; 179 | process.env.SKIP_PUBLISH = "true"; 180 | 181 | const out = await exec("node publish-extensions", { quiet: true, ghtoken: true }); 182 | if (out && out.stderr.includes("[FAIL] Could not process extension:")) { 183 | console.error( 184 | `There was an error while trying to build ${extID}. Reverting back to the previous state of extensions.json.`, 185 | ); 186 | await fs.promises.writeFile( 187 | "./extensions.json", 188 | originalList + "\n", // add newline at EOF 189 | { encoding: "utf8" }, 190 | ); 191 | } else { 192 | console.info("Built extension successfully"); 193 | console.info(`Feel free to use the message below for your commit:\r\nAdded \`${extID}\``); 194 | } 195 | })(); 196 | -------------------------------------------------------------------------------- /bin/publish-extensions: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("../publish-extensions"); 4 | -------------------------------------------------------------------------------- /diff-extensions.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2021 Gitpod and others 3 | * 4 | * This program and the accompanying materials are made available under the 5 | * terms of the Eclipse Public License v. 2.0 which is available at 6 | * http://www.eclipse.org/legal/epl-2.0. 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | ********************************************************************************/ 10 | 11 | // @ts-check 12 | const fs = require("fs"); 13 | const Octokit = require("octokit").Octokit; 14 | 15 | /** 16 | * 17 | * @param {Readonly} original 18 | * @param {Readonly} current 19 | * @returns {string[]} 20 | */ 21 | function diff(original, current) { 22 | const changes = []; 23 | for (const id in current) { 24 | const extension = current[id]; 25 | if (original.hasOwnProperty(id)) { 26 | if (JSON.stringify(original[id]) !== JSON.stringify(extension)) { 27 | changes.push(id); 28 | } 29 | } else { 30 | changes.push(id); 31 | } 32 | } 33 | return changes; 34 | } 35 | 36 | (async () => { 37 | const token = process.env.GITHUB_TOKEN; 38 | if (!token) { 39 | throw new Error("GITHUB_TOKEN env var is not set"); 40 | } 41 | const [owner, repo] = ["open-vsx", "publish-extensions"]; 42 | 43 | const octokit = new Octokit({ auth: token }); 44 | const fileResponse = await octokit.rest.repos.getContent({ 45 | owner, 46 | repo, 47 | path: "extensions.json", 48 | mediaType: { format: "raw" }, 49 | }); 50 | if (!(typeof fileResponse.data === "string")) { 51 | return undefined; 52 | } 53 | const manifest = JSON.parse(fileResponse.data); 54 | const newExtensions = JSON.parse(await fs.promises.readFile("./extensions.json", "utf-8")); 55 | const updatedExtensions = diff(manifest, newExtensions); 56 | console.log([...updatedExtensions].join(",") || ","); 57 | })(); 58 | -------------------------------------------------------------------------------- /docs/direct_publish_setup.md: -------------------------------------------------------------------------------- 1 | # Publishing an extension to Open VSX on your own 2 | 3 | We advise extension authors to publish their extensions to Open VSX as a part of their CI/CD process. See [our reasoning why that is](https://github.com/open-vsx/publish-extensions/blob/master/CONTRIBUTING.md#when-to-add-an-extension). 4 | 5 | To make the Open VSX publishing process easier, we have provided a template of a GitHub Actions workflow. 6 | 7 | The template performs the following: 8 | 9 | - Publishing to GitHub Releases, the Microsoft Marketplace and Open VSX 10 | - Uploading the `.vsix` package to GitHub Releases as assets 11 | - Manually triggers releases and publishing to Open VSX and/or Microsoft Marketplace. 12 | - Automatic triggers from new GitHub Releases 13 | 14 | ## Setup VS Code extension publishing CI workflow 15 | 16 | 1. First, follow the [Publishing Extensions](https://github.com/eclipse/openvsx/wiki/Publishing-Extensions) doc (only steps 1-4 are required) and take note of the access token that is returned, as you will require it in the next step. 17 | 18 | 2. To run the GitHub Action above, you need to setup two repository secrets for the Action to use: 19 | 20 | - `VSCE_PAT` - the token for publishing to Microsoft's Marketplace (["Get a Personal Access Token" in VS Code's docs](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#get-a-personal-access-token)) 21 | - `OVSX_PAT` - the token for publishing to Open VSX. This token was displayed in your https://open-vsx.org user dashboard. 22 | 23 | 3. In your extension repo, [create a GitHub Action](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions#create-an-example-workflow) with the contents of [this template](exampleCI.yaml). You can customize this however you like, for instance: 24 | - you can customize the [release tag](exampleCI.yaml#L60) or the [release name](exampleCI.yaml#L108) 25 | - you can customize what the [packaging process behaves and executes](exampleCI.yaml#L32) 26 | - you can customize the [workflow triggers](exampleCI.yaml#L2) to better fit in your workflow. See [Events that trigger workflows](https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows) for all the possible options. 27 | 4. Now you can test whether your config works by committing the Action file and running it from the Actions tab in your repo (select your workflow on the left and hit Run Workflow on the top right). 28 | - note this will not work if you have removed the `workflow_dispatch` trigger for the workflow. You will need to trigger the Action some other way (e.g. creating a blank GitHub Release) 29 | -------------------------------------------------------------------------------- /docs/exampleCI.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: 5 | - published 6 | workflow_dispatch: 7 | inputs: 8 | publishMS: 9 | description: "Publish to the Microsoft Marketplace" 10 | type: boolean 11 | required: true 12 | default: "true" 13 | publishOVSX: 14 | description: "Publish to Open VSX" 15 | type: boolean 16 | required: true 17 | default: "true" 18 | publishGH: 19 | description: "Publish to GitHub Releases" 20 | type: boolean 21 | required: true 22 | default: "true" 23 | 24 | jobs: 25 | package: 26 | name: Package 27 | runs-on: ubuntu-latest 28 | outputs: 29 | packageName: ${{ steps.setup.outputs.packageName }} 30 | tag: ${{ steps.setup-tag.outputs.tag }} 31 | version: ${{ steps.setup-tag.outputs.version }} 32 | steps: 33 | - uses: actions/checkout@v2 34 | - uses: actions/setup-node@v2 35 | with: 36 | node-version: 14 37 | registry-url: https://registry.npmjs.org/ 38 | 39 | - name: Install dependencies 40 | run: npm i 41 | 42 | - name: Setup package path 43 | id: setup 44 | run: echo "::set-output name=packageName::$(node -e "console.log(require('./package.json').name + '-' + require('./package.json').version + '.vsix')")" 45 | 46 | - name: Package 47 | run: | 48 | npx vsce package --out ${{ steps.setup.outputs.packageName }} 49 | 50 | - uses: actions/upload-artifact@v2 51 | with: 52 | name: ${{ steps.setup.outputs.packageName }} 53 | path: ./${{ steps.setup.outputs.packageName }} 54 | if-no-files-found: error 55 | 56 | - name: Setup tag 57 | id: setup-tag 58 | run: | 59 | $version = (Get-Content ./package.json -Raw | ConvertFrom-Json).version 60 | Write-Host "tag: release/$version" 61 | Write-Host "::set-output name=tag::release/$version" 62 | Write-Host "::set-output name=version::$version" 63 | shell: pwsh 64 | 65 | publishMS: 66 | name: Publish to VS marketplace 67 | runs-on: ubuntu-latest 68 | needs: package 69 | if: github.event.inputs.publishMS == 'true' 70 | steps: 71 | - uses: actions/checkout@v2 72 | - uses: actions/download-artifact@v2 73 | with: 74 | name: ${{ needs.package.outputs.packageName }} 75 | - name: Publish to VS marketplace 76 | run: npx vsce publish --packagePath ./${{ needs.package.outputs.packageName }} -p ${{ secrets.VSCE_PAT }} 77 | 78 | publishOVSX: 79 | name: Publish to Open VSX 80 | runs-on: ubuntu-latest 81 | needs: package 82 | if: github.event.inputs.publishOVSX == 'true' 83 | steps: 84 | - uses: actions/checkout@v2 85 | - uses: actions/download-artifact@v2 86 | with: 87 | name: ${{ needs.package.outputs.packageName }} 88 | - name: Publish to Open VSX 89 | run: npx ovsx publish ./${{ needs.package.outputs.packageName }} -p ${{ secrets.OVSX_PAT }} 90 | 91 | publishGH: 92 | name: Publish to GitHub releases 93 | runs-on: ubuntu-latest 94 | needs: package 95 | if: github.event.inputs.publishGH == 'true' 96 | steps: 97 | - uses: actions/download-artifact@v2 98 | with: 99 | name: ${{ needs.package.outputs.packageName }} 100 | 101 | - name: Create Release 102 | id: create-release 103 | uses: actions/create-release@v1 104 | env: 105 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 106 | with: 107 | tag_name: ${{ needs.package.outputs.tag }} 108 | release_name: Release ${{ needs.package.outputs.version }} 109 | draft: false 110 | prerelease: false 111 | 112 | - name: Upload assets to a Release 113 | uses: AButler/upload-release-assets@v2.0 114 | with: 115 | files: ${{ needs.package.outputs.packageName }} 116 | release-tag: ${{ needs.package.outputs.tag }} 117 | repo-token: ${{ secrets.GITHUB_TOKEN }} 118 | -------------------------------------------------------------------------------- /docs/extension_issues.md: -------------------------------------------------------------------------------- 1 | # Bugs with extensions 2 | 3 | ## Functional bugs in VS Code extensions 4 | 5 | If an extension you downloaded from Open VSX isn't functioning as expected, please follow these steps to debug: 6 | 7 | 1. check the extension version on [Open VSX](https://open-vsx.org/) (you can see the latest one in the right sidebar) 8 | 2. check the extension version on the [VS Code Marketplace](https://marketplace.visualstudio.com/) (you can find the version on the bottom of the right-hand side bar) 9 | 3. If the versions don't match, it's probably a problem with the publishing process, so continue to the next chapter of this guide 10 | 4. If they are the same version, visit the extension's source repository to investigate the issue further. 11 | 12 | **Important:** We (`publish-extensions` maintainers) cannot help with functionally broken extensions; these issues should be raised with the respective maintainers. 13 | 14 | If the extension has been unmaintained for a longer period of time, and maintainers are not responding to requests to publish or update the extension, [raise an issue](https://github.com/open-vsx/publish-extensions/issues/new) and select Other as the GitHub issue template, adding the necessary information (we publish the latest version by building manually and remove the extension from this repository). 15 | 16 | ## Errors in the publishing process 17 | 18 | If an extension on Open VSX is outdated or has never been successfully published (despite being listed in [`extensions.json`](https://github.com/open-vsx/publish-extensions/blob/HEAD/extensions.json)), the cause is likely one of the following: 19 | 20 | 1. The extension has some abnormal build prerequisites, we build everything inside the same Ubuntu VM, which might cause problems like: 21 | - a CLI tool is not installed 22 | - the extension requires an older/newer version of Node.js: we are using Node 14 by default, although this can be fixed by extensions specifying a different Node version using the [`.nvmrc`](https://github.com/nvm-sh/nvm#nvmrc) file in their repository. 23 | - the extension has issues building on the latest LTS of Ubuntu server (we use `ubuntu-latest` for our jobs, you can take a look at [GitHub's Docs](https://github.com/actions/runner-images#available-images) to see what that currently stands for) 24 | 2. The extension requires additional commands to be executed to build successfully. 25 | - if you want a quick and easy fix you can try adding a `prepublish` property to the extension in `extensions.json` to set a command to be executed before packaging up the extension, right after installing the project's dependencies. 26 | 27 | The best way to solve issues of failed publishing is to publish the extension from its own repository, not this one. 28 | 29 | - If you are the extension author, you can use [this document](direct_publish_setup.md) as a guide on how to set up a CI publishing process. 30 | - If you are a community member you can raise an issue (please check for existing issues) using [this template](external_contribution_request.md). If the maintainers are willing to accept a contribution you can use the [same document listed in the point above](direct_publish_setup.md) to quickly setup a CI job with GitHub Actions. 31 | -------------------------------------------------------------------------------- /docs/external_contribution_request.md: -------------------------------------------------------------------------------- 1 | ## Issue title 2 | 3 | ```md 4 | Publish `EXTENSION_NAME` to Open VSX 5 | ``` 6 | 7 | ## Issue body 8 | 9 | ```md 10 | Dear extension author, 11 | Please publish this extension to the Open VSX marketplace. 12 | 13 | ## Context 14 | 15 | Unfortunately, as Microsoft prohibits usages of the Microsoft marketplace by any other products or redistribution of `.vsix` files from it, in order to use VS Code extensions in non-Microsoft products, we kindly ask that you take ownership of the VS Code extension namespace in [Open VSX](https://open-vsx.org/) and publish this extension on Open VSX. 16 | 17 | ## What is Open VSX? Why does it exist? 18 | 19 | Open VSX is a vendor neutral alternative to the MS marketplace used by most other derivatives of VS Code like [VSCodium](https://vscodium.com/), [Gitpod](https://www.gitpod.io), [OpenVSCode](https://github.com/gitpod-io/openvscode-server), [Theia](https://theia-ide.org/)-based IDEs, and so on. 20 | 21 | You can read on about Open VSX at the Eclipse Foundation's [Open VSX FAQ](https://www.eclipse.org/legal/open-vsx-registry-faq/). 22 | 23 | ## How can you publish to Open VSX? 24 | 25 | The docs to publish an extension can be found [here](https://github.com/eclipse/openvsx/wiki/Publishing-Extensions). This process is straightforward and shouldn't take too long. Essentially, you need an authentication token and to execute the `ovsx publish` command to publish your extension. There's also [a doc](https://github.com/open-vsx/publish-extensions/blob/master/docs/direct_publish_setup.md) explaining the whole process with an example GitHub Action workflow. 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/understanding_reports.md: -------------------------------------------------------------------------------- 1 | # Understanding statistics 2 | 3 | This document is here to help you understand reports from [the nightly publishing job](https://github.com/open-vsx/publish-extensions/actions/workflows/publish-extensions.yml). 4 | 5 | ## How to gather these statistics 6 | 7 | If you click on a job in the GitHub Actions tab, there is an `Artifacts` section at the bottom of the page, from which you can download the `report`, which after unarchiving reveals three files: `result.md`, `stat.json` and `meta.json`. 8 | 9 | ## `stat.json` 10 | 11 | This is the machine-readable data that the next file - `result.md` is generated from. In it, you can find 9 different categories of extensions: 12 | 13 | - `upToDate` - these extensions are the extensions, which have the same version published to Open VSX as well as the Microsoft Marketplace. 14 | - `outdated` are all of the extensions, which have versions on Open VSX, which are behind the ones on the Microsoft Marketplace. 15 | - `unstable` is the category, which has all extensions, that are most likely incorrectly published from the extensions' nightly or beta builds, since on the Microsoft Marketplace has a version which is smaller than the one on Open VSX. 16 | - `notInOpen` includes extensions that simply failed to ever be published to Open VSX, which means they should get special attention - fix them or remove them :) 17 | - `notInMs` - extensions that aren't published on the Microsoft Marketplace 18 | - `failed` - the extensions that for some reason failed with their publishing. 19 | - `msPublished` - all extensions published by Microsoft Corporation. 20 | - `hitMiss` - extensions which, in MTD, have been updated on Open VSX within 2 days after the Microsoft Marketplace. 21 | - `resolutions` is a list of all extensions and the way they have been resolved: `latest`, `matchedLatest`. `releaseTag`, `tag` or `releaseAsset`. 22 | 23 | ## `result.md` 24 | 25 | This file is the one that should provide a quick overview of how the repository is doing. It has many percentages, numbers and sections, so that you can quickly take a look and get the information you want. These are mostly made from the `stat.json` file and pretty self-explanatory, but there are some that are a bit more complex: 26 | 27 | ### `Weighted publish percentage` 28 | 29 | This metric's goal is to provide the one number you need to see if the big and most used extensions are up-to-date and existing on Open VSX. This value is computed as follows (in pseudo-code): 30 | 31 | ```ts 32 | const upToDateInstalls = sum(upToDate); // a sum of all installs on the Microsoft Marketplace of all up-to-date extensions 33 | const totalInstalls = sum(upToDate); // a sum of all install from the Microsoft Marketplace across both up-to-date extensions, as well as outdated, unstable and failing to publish at all 34 | 35 | const weightedPublishPercentage = upToDateInstalls / totalInstalls; 36 | ``` 37 | 38 | ### Microsoft-published extensions 39 | 40 | In the `Summary`, you can see a how many extensions published by Microsoft are defined in our repo, how many of them are outdated and how many of them are unstable. For further details with all of the failing extensions, refer to the `MS extensions` section in the file. Under there, you can find more details, like the specific IDs of the extensions and their install counts. 41 | 42 | ## `meta.json` 43 | 44 | Contains metadata that can be used by other jobs wanting to examine data from previous runs. It is not intended to be read by humans. 45 | -------------------------------------------------------------------------------- /extension-control/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "malicious": [ 3 | "kyntrack.log-matrix", 4 | "bipro.bipro-dev", 5 | "Equinusocio.vsc-material-theme", 6 | "Equinusocio.vsc-material-theme-icons", 7 | "498.pythonformat", 8 | "prada555.Theme-Acai-ported-1.0.0", 9 | "prada555.Theme-Active4D-ported-1.0.0", 10 | "AllenBarry.Solid" 11 | ], 12 | "search": [ 13 | { 14 | "query": "github", 15 | "preferredResults": [ 16 | "github.vscode-pull-request-github", 17 | "github.github-vscode-theme" 18 | ] 19 | }, 20 | { 21 | "query": "java", 22 | "preferredResults": [ 23 | "vscjava.vscode-java-pack" 24 | ] 25 | }, 26 | { 27 | "query": "java extension pack", 28 | "preferredResults": [ 29 | "vscjava.vscode-java-pack" 30 | ] 31 | }, 32 | { 33 | "query": "azure", 34 | "preferredResults": [ 35 | "ms-azuretools.vscode-azureappservice" 36 | ] 37 | }, 38 | { 39 | "query": "codegpt", 40 | "preferredResults": [ 41 | "DanielSanMedium.dscodegpt" 42 | ] 43 | } 44 | ], 45 | "deprecated": { 46 | "andrewhertog.codex-editor-extension": true, 47 | "dieghernan.oveflow-theme": { 48 | "disallowInstall": true, 49 | "extension": { 50 | "id": "dieghernan.overflow-theme", 51 | "displayName": "Overflow Theme" 52 | } 53 | }, 54 | "decodetalkers.neocmake-lsp-vscode": { 55 | "disallowInstall": false, 56 | "extension": { 57 | "id": "decodetalkers.neocmakelsp-vscode", 58 | "displayName": "CMake Language" 59 | } 60 | }, 61 | "msjsdiag.debugger-for-chrome": { 62 | "disallowInstall": true, 63 | "extension": { 64 | "id": "ms-vscode.js-debug", 65 | "displayName": "JavaScript Debugger" 66 | } 67 | }, 68 | "abusaidm.html-snippets": true, 69 | "ms-vscode.vscode-typescript-tslint-plugin": true, 70 | "auchenberg.vscode-browser-preview": { 71 | "disallowInstall": false, 72 | "extension": { 73 | "id": "ms-vscode.live-server", 74 | "displayName": "Live Preview" 75 | } 76 | }, 77 | "Shan.code-settings-sync": { 78 | "disallowInstall": true, 79 | "additionalInfo": "Please use the built-in [Settings Sync](https://aka.ms/vscode-settings-sync-help) functionality instead." 80 | }, 81 | "tiehuis.zig": { 82 | "disallowInstall": true, 83 | "extension": { 84 | "id": "ziglang.vscode-zig", 85 | "displayName": "Zig Language" 86 | } 87 | }, 88 | "prime31.zig": { 89 | "disallowInstall": true, 90 | "extension": { 91 | "id": "ziglang.vscode-zig", 92 | "displayName": "Zig Language" 93 | } 94 | }, 95 | "gencay.vscode-chatgpt": { 96 | "disallowInstall": true, 97 | "extension": { 98 | "id": "genieai.chatgpt-vscode", 99 | "displayName": "Chat" 100 | } 101 | }, 102 | "dbaeumer.jshint": { 103 | "disallowInstall": true, 104 | "extension": { 105 | "id": "dbaeumer.vscode-eslint", 106 | "displayName": "ESLint" 107 | } 108 | }, 109 | "wwm.better-align": { 110 | "disallowInstall": true, 111 | "extension": { 112 | "id": "Chouzz.vscode-better-align", 113 | "displayName": "Better Align" 114 | } 115 | }, 116 | "rust-lang.rust": { 117 | "disallowInstall": true, 118 | "extension": { 119 | "id": "rust-lang.rust-analyzer", 120 | "displayName": "rust-analyzer" 121 | } 122 | }, 123 | "KnisterPeter.vscode-github": { 124 | "disallowInstall": true, 125 | "extension": { 126 | "id": "GitHub.vscode-pull-request-github", 127 | "displayName": "GitHub Pull Requests and Issues" 128 | } 129 | }, 130 | "antfu.vue-i18n-ally": { 131 | "disallowInstall": true, 132 | "extension": { 133 | "id": "lokalise.i18n-ally", 134 | "displayName": "i18n Ally" 135 | } 136 | }, 137 | "antfu.i18n-ally": { 138 | "disallowInstall": true, 139 | "extension": { 140 | "id": "lokalise.i18n-ally", 141 | "displayName": "i18n Ally" 142 | } 143 | }, 144 | "redhat.vscode-didact": { 145 | "disallowInstall": false, 146 | "extension": { 147 | "id": "vsls-contrib.codetour", 148 | "displayName": "CodeTour" 149 | } 150 | }, 151 | "EmberTooling.prettier-for-handlebars-vscode": { 152 | "disallowInstall": true, 153 | "extension": { 154 | "id": "esbenp.prettier-vscode", 155 | "displayName": "Prettier - Code formatter" 156 | } 157 | }, 158 | "silvenon.mdx": { 159 | "disallowInstall": true, 160 | "extension": { 161 | "id": "unifiedjs.vscode-mdx", 162 | "displayName": "VSCode MDX" 163 | } 164 | }, 165 | "CoenraadS.bracket-pair-colorizer-2": { 166 | "settings": [ 167 | "editor.bracketPairColorization.enabled", 168 | "editor.guides.bracketPairs" 169 | ] 170 | }, 171 | "CoenraadS.bracket-pair-colorizer": { 172 | "settings": [ 173 | "editor.bracketPairColorization.enabled", 174 | "editor.guides.bracketPairs" 175 | ] 176 | }, 177 | "idleberg.innosetup": { 178 | "disallowInstall": true, 179 | "extension": { 180 | "id": "Chouzz.vscode-innosetup", 181 | "displayName": "Inno Setup" 182 | } 183 | }, 184 | "ModyQyW.vscode-uni-app-schemas": { 185 | "disallowInstall": true, 186 | "extension": { 187 | "id": "uni-helper.uni-app-schemas-vscode", 188 | "displayName": "uni-app-schemas" 189 | } 190 | }, 191 | "ModyQyW.vscode-uni-app-snippets": { 192 | "disallowInstall": true, 193 | "extension": { 194 | "id": "uni-helper.uni-app-snippets-vscode", 195 | "displayName": "uni-app-snippets" 196 | } 197 | }, 198 | "ModyQyW.vscode-uni-cloud-snippets": { 199 | "disallowInstall": true, 200 | "extension": { 201 | "id": "uni-helper.uni-cloud-snippets-vscode", 202 | "displayName": "uni-cloud-snippets" 203 | } 204 | }, 205 | "ModyQyW.vscode-uni-helper": { 206 | "disallowInstall": true, 207 | "extension": { 208 | "id": "uni-helper.uni-helper-vscode", 209 | "displayName": "uni-helper" 210 | } 211 | }, 212 | "ModyQyW.vscode-uni-ui-snippets": { 213 | "disallowInstall": true, 214 | "extension": { 215 | "id": "uni-helper.uni-ui-snippets-vscode", 216 | "displayName": "uni-ui-snippets" 217 | } 218 | }, 219 | "redhat.vscode-extension-serverless-workflow-editor": { 220 | "disallowInstall": true, 221 | "extension": { 222 | "id": "kie-group.swf-vscode-extension", 223 | "displayName": "KIE Serverless Workflow Editor" 224 | } 225 | }, 226 | "errata-ai.vale-server": { 227 | "disallowInstall": true, 228 | "extension": { 229 | "id": "chrischinchilla.vale-vscode", 230 | "displayName": "Vale VSCode" 231 | } 232 | }, 233 | "bungcip.better-toml": { 234 | "disallowInstall": true, 235 | "extension": { 236 | "id": "tamasfe.even-better-toml", 237 | "displayName": "Even Better TOML" 238 | } 239 | }, 240 | "snyk-security.vscode-vuln-cost": { 241 | "disallowInstall": true, 242 | "extension": { 243 | "id": "snyk-security.snyk-vulnerability-scanner", 244 | "displayName": "Snyk Security - Code, Open Source Dependencies, IaC Configurations" 245 | } 246 | }, 247 | "wingrunr21.vscode-ruby": { 248 | "disallowInstall": true, 249 | "extension": { 250 | "id": "Shopify.ruby-lsp", 251 | "displayName": "Ruby LSP" 252 | } 253 | }, 254 | "rebornix.Ruby": { 255 | "disallowInstall": true, 256 | "extension": { 257 | "id": "Shopify.ruby-lsp", 258 | "displayName": "Ruby LSP" 259 | } 260 | }, 261 | "Esri.arcgis-jsapi-snippets": { 262 | "disallowInstall": true, 263 | "extension": { 264 | "id": "Esri.arcgis-maps-sdk-js-snippets", 265 | "displayName": "ArcGIS Maps SDK for JavaScript Snippets" 266 | } 267 | }, 268 | "ligolang-publish.ligo-debugger-vscode": { 269 | "disallowInstall": true, 270 | "extension": { 271 | "id": "ligolang-publish.ligo-vscode", 272 | "displayName": "ligo-vscode" 273 | } 274 | }, 275 | "Equinusocio.vsc-community-material-theme": { 276 | "disallowInstall": true, 277 | "extension": { 278 | "id": "Equinusocio.vsc-material-theme", 279 | "displayName": "Material Theme" 280 | } 281 | }, 282 | "jetmartin.apicurio": { 283 | "disallowInstall": true, 284 | "extension": { 285 | "id": "apicurio.apicurio-registry-explorer", 286 | "displayName": "Apicurio registry" 287 | } 288 | }, 289 | "biomejs.biome-nightly": { 290 | "disallowInstall": true, 291 | "extension": { 292 | "id": "biomejs.biome", 293 | "displayName": "Biome" 294 | } 295 | }, 296 | "ZixuanChen.vitest-explorer": { 297 | "disallowInstall": true, 298 | "extension": { 299 | "id": "vitest.explorer", 300 | "displayName": "Vitest" 301 | } 302 | }, 303 | "cortex-debug.svd-viewer": { 304 | "disallowInstall": true, 305 | "extension": { 306 | "id": "eclipse-cdt.peripheral-inspector", 307 | "displayName": "Eclipse CDT Cloud" 308 | } 309 | }, 310 | "iocave.customize-ui": true, 311 | "iocave.monkey-patch": true, 312 | "redhat.vscode-wsdl2rest": true, 313 | "ms-vscode.node-debug2": true, 314 | "ms-vscode.node-debug": true, 315 | "HookyQR.beautify": true, 316 | "tht13.python": true, 317 | "lonefy.vscode-JS-CSS-HTML-formatter": true, 318 | "ikappas.phpcs": true, 319 | "heptio.jsonnet": true, 320 | "eg2.vscode-npm-script": true, 321 | "virgilsisoe.python-auto-import": true, 322 | "ethan-reesor.vscode-go-test-adapter": true, 323 | "imperez.smarty": { 324 | "disallowInstall": true, 325 | "extension": { 326 | "id": "aswinkumar863.smarty-template-support", 327 | "displayName": "Smarty Template Support" 328 | } 329 | }, 330 | "kavod-io.vscode-jest-test-adapter": { 331 | "disallowInstall": true, 332 | "extension": { 333 | "id": "Orta.vscode-jest", 334 | "displayName": "Jest" 335 | } 336 | }, 337 | "projektanker.code-butler": { 338 | "disallowInstall": true, 339 | "extension": { 340 | "id": "just-seba.vscode-code-butler", 341 | "displayName": "Code Butler" 342 | } 343 | }, 344 | "redhat.atlasmap-viewer": true, 345 | "AdaCore.ada-debug": { 346 | "disallowInstall": true, 347 | "extension": { 348 | "id": "AdaCore.ada", 349 | "displayName": "Ada & SPARK" 350 | } 351 | }, 352 | "mgt19937.typst-preview": { 353 | "disallowInstall": true, 354 | "extension": { 355 | "id": "myriad-dreamin.tinymist", 356 | "displayName": "Tinymist Typst" 357 | } 358 | }, 359 | "serayuzgur.crates": { 360 | "disallowInstall": true, 361 | "extension": { 362 | "id": "fill-labs.dependi", 363 | "displayName": "Dependi" 364 | } 365 | }, 366 | "alygin.vscode-tlaplus": { 367 | "disallowInstall": true, 368 | "extension": { 369 | "id": "tlaplus.vscode-ide", 370 | "displayName": "TLA+ (Temporal Logic of Actions)" 371 | } 372 | }, 373 | "alygin.vscode-tlaplus-nightly": { 374 | "disallowInstall": true, 375 | "extension": { 376 | "id": "tlaplus.vscode-ide", 377 | "displayName": "TLA+ (Temporal Logic of Actions)" 378 | } 379 | }, 380 | "cweijan.vscode-autohotkey-plus": { 381 | "disallowInstall": true, 382 | "extension": { 383 | "id": "mark-wiemer.vscode-autohotkey-plus-plus", 384 | "displayName": "AutoHotkey Plus Plus" 385 | } 386 | }, 387 | "mark-wiemer.ahk-v1-formatter": { 388 | "disallowInstall": true, 389 | "extension": { 390 | "id": "mark-wiemer.vscode-autohotkey-plus-plus", 391 | "displayName": "AutoHotkey Plus Plus" 392 | } 393 | }, 394 | "redhat.vscode-camelk": { 395 | "disallowInstall": true, 396 | "extension": { 397 | "id": "redhat.apache-camel-extension-pack", 398 | "displayName": "Extension Pack for Apache Camel by Red Hat" 399 | } 400 | }, 401 | "kleinesfilmroellchen.serenity-dsl-syntaxhighlight": { 402 | "disallowInstall": true, 403 | "extension": { 404 | "id": "kleinesfilmroellchen.serenity-dsl-syntaxhighlight", 405 | "displayName": "SerenityOS DSL Syntax Highlight" 406 | } 407 | }, 408 | "Boundary.baml-extension-preview": { 409 | "disallowInstall": true, 410 | "extension": { 411 | "id": "Boundary.baml-extension", 412 | "displayName": "BAML" 413 | } 414 | }, 415 | "lifeart.vscode-ember-unstable": { 416 | "disallowInstall": true, 417 | "extension": { 418 | "id": "EmberTooling.vscode-ember", 419 | "displayName": "Ember Language Server" 420 | } 421 | }, 422 | "ms-vscode.azure-account": { 423 | "disallowInstall": true, 424 | "extension": { 425 | "id": "ms-azuretools.vscode-azureresourcegroups", 426 | "displayName": "Azure Resources" 427 | } 428 | }, 429 | "adamcowley.neo4j-vscode": { 430 | "disallowInstall": true, 431 | "extension": { 432 | "id": "neo4j-extensions.neo4j-for-vscode", 433 | "displayName": "Neo4j for VS Code" 434 | } 435 | }, 436 | "KylinIdeTeam.debug": { 437 | "disallowInstall": false, 438 | "extension": { 439 | "id": "KylinIdeTeam.kylin-debug", 440 | "displayName": "Kylin Debug" 441 | } 442 | }, 443 | "KylinIdeTeam.vscode-clangd": { 444 | "disallowInstall": false, 445 | "extension": { 446 | "id": "KylinIdeTeam.kylin-clangd", 447 | "displayName": "Kylin Clangd" 448 | } 449 | }, 450 | "KylinIdeTeam.project-manager": { 451 | "disallowInstall": false, 452 | "extension": { 453 | "id": "KylinIdeTeam.kylin-project-manager", 454 | "displayName": "Kylin Project Manager" 455 | } 456 | }, 457 | "KylinIdeTeam.python": { 458 | "disallowInstall": false, 459 | "extension": { 460 | "id": "KylinIdeTeam.kylin-python", 461 | "displayName": "Kylin Python(with jedi language server)" 462 | } 463 | }, 464 | "ms-vsts.team": { 465 | "disallowInstall": false, 466 | "extension": { 467 | "id": "ms-vsts.team", 468 | "displayName": "Azure Repos" 469 | } 470 | }, 471 | "redhat.project-initializer": { 472 | "disallowInstall": false, 473 | "extension": { 474 | "id": "redhat.vscode-openshift-connector", 475 | "displayName": "OpenShift Toolkit" 476 | } 477 | }, 478 | "SridharMocherla.bazel-kotlin-vscode-extension": { 479 | "disallowInstall": true, 480 | "extension": { 481 | "id": "Brex.bazel-kotlin", 482 | "displayName": "Bazel Kotlin" 483 | } 484 | }, 485 | "DrMerfy.overtype": true, 486 | "jroesch.lean": true, 487 | "moalamri.inline-fold": true, 488 | "MS-CEINTL.vscode-language-pack-bg": true, 489 | "MS-CEINTL.vscode-language-pack-en-GB": true, 490 | "MS-CEINTL.vscode-language-pack-hu": true, 491 | "MS-CEINTL.vscode-language-pack-id": true, 492 | "MS-CEINTL.vscode-language-pack-nl": true, 493 | "MS-CEINTL.vscode-language-pack-uk": true 494 | }, 495 | "migrateToPreRelease": { 496 | "julialang.language-julia-insider": { 497 | "id": "julialang.language-julia", 498 | "displayName": "Julia" 499 | }, 500 | "ms-vscode.PowerShell-Preview": { 501 | "id": "ms-vscode.PowerShell", 502 | "displayName": "PowerShell" 503 | }, 504 | "hediet.vscode-drawio-insiders-build": { 505 | "id": "hediet.vscode-drawio", 506 | "displayName": "Draw.io Integration" 507 | } 508 | }, 509 | "extensionsEnabledWithPreRelease": [] 510 | } -------------------------------------------------------------------------------- /extension-control/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "object", 4 | "properties": { 5 | "malicious": { 6 | "type": "array", 7 | "items": { 8 | "type": "string" 9 | } 10 | }, 11 | "migrateToPreRelease": { 12 | "type": "object", 13 | "additionalProperties": { 14 | "type": "object", 15 | "properties": { 16 | "id": { 17 | "type": "string" 18 | }, 19 | "displayName": { 20 | "type": "string" 21 | }, 22 | "migrateStorage": { 23 | "type": "boolean" 24 | }, 25 | "engine": { 26 | "type": "string" 27 | } 28 | }, 29 | "required": ["id", "displayName"], 30 | "additionalProperties": false 31 | } 32 | }, 33 | "deprecated": { 34 | "type": "object", 35 | "additionalProperties": { 36 | "oneOf": [ 37 | { 38 | "type": "boolean" 39 | }, 40 | { 41 | "type": "object", 42 | "properties": { 43 | "disallowInstall": { 44 | "type": "boolean" 45 | }, 46 | "extension": { 47 | "type": "object", 48 | "properties": { 49 | "id": { 50 | "type": "string" 51 | }, 52 | "displayName": { 53 | "type": "string" 54 | } 55 | }, 56 | "required": ["id", "displayName"], 57 | "additionalProperties": false 58 | }, 59 | "settings": { 60 | "type": "array", 61 | "items": { 62 | "type": "string" 63 | } 64 | }, 65 | "additionalInfo": { 66 | "type": "string" 67 | } 68 | }, 69 | "additionalProperties": false 70 | } 71 | ] 72 | } 73 | }, 74 | "search": { 75 | "type": "array", 76 | "items": { 77 | "type": "object", 78 | "properties": { 79 | "query": { 80 | "type": "string" 81 | }, 82 | "preferredResults": { 83 | "type": "array", 84 | "items": { 85 | "type": "string" 86 | } 87 | } 88 | }, 89 | "additionalProperties": false 90 | } 91 | }, 92 | "extensionsEnabledWithPreRelease": { 93 | "type": "array", 94 | "items": { 95 | "type": "string" 96 | } 97 | } 98 | }, 99 | "required": ["malicious"], 100 | "additionalProperties": false 101 | } 102 | -------------------------------------------------------------------------------- /extension-control/update.ts: -------------------------------------------------------------------------------- 1 | // This script automatically updates ./extensions.json with the latest changes from upstream. 2 | // usage: bun update.ts 3 | 4 | import { diff } from "jest-diff"; 5 | import path from "path"; 6 | 7 | // https://github.com/microsoft/vscode/blob/a2acd131e47500cf4bd7d602626f0b54ab266904/src/vs/platform/extensionManagement/common/extensionManagement.ts#L314 8 | interface ISearchPrefferedResults { 9 | readonly query?: string; 10 | readonly preferredResults?: string[]; 11 | } 12 | 13 | export type IStringDictionary = Record; 14 | 15 | // https://github.com/microsoft/vscode/blob/cb1514f9a64ab342f94092f79bdf1a768635d96f/src/vs/platform/extensionManagement/common/extensionGalleryService.ts#L572-L591 16 | interface IRawExtensionsControlManifest { 17 | malicious: string[]; 18 | migrateToPreRelease?: IStringDictionary<{ 19 | id: string; 20 | displayName: string; 21 | migrateStorage?: boolean; 22 | engine?: string; 23 | }>; 24 | deprecated?: IStringDictionary< 25 | | boolean 26 | | { 27 | disallowInstall?: boolean; 28 | extension?: { 29 | id: string; 30 | displayName: string; 31 | }; 32 | settings?: string[]; 33 | additionalInfo?: string; 34 | } 35 | >; 36 | search?: ISearchPrefferedResults[]; 37 | extensionsEnabledWithPreRelease?: string[]; 38 | } 39 | 40 | const existsOnOpenVSX = async (id: string) => { 41 | const response = await fetch(`https://open-vsx.org/api/${id.replace(/\./g, "/")}`); 42 | if (response.ok) { 43 | console.log(`Extension ${id} exists on OpenVSX.`); 44 | return true; 45 | } 46 | 47 | console.log(`Extension ${id} does not exist on OpenVSX. (HTTP ${response.status})`); 48 | return false; 49 | }; 50 | 51 | const latestData = await fetch("https://main.vscode-cdn.net/extensions/marketplace.json"); 52 | const latestJson = (await latestData.json()) as IStringDictionary; 53 | 54 | const localFile = Bun.file(path.resolve(__dirname, "./extensions.json")); 55 | const localData = JSON.parse(await localFile.text()) as IStringDictionary; 56 | const updatedData = structuredClone(localData); 57 | 58 | for (const key of Object.values(latestJson.malicious)) { 59 | if (!localData.malicious.includes(key)) { 60 | const exists = await existsOnOpenVSX(key); 61 | if (exists) { 62 | updatedData.malicious.push(key); 63 | } 64 | } 65 | } 66 | 67 | const missingDependency = []; 68 | for (const key of Object.keys(latestJson.deprecated)) { 69 | if (key in localData.deprecated) { 70 | continue; 71 | } 72 | 73 | const extensionsToCheck = [key]; 74 | const value = latestJson.deprecated[key]; 75 | if (typeof value === "object" && value.extension) { 76 | extensionsToCheck.push(value.extension.id); 77 | } 78 | 79 | // Ensure both extensions exist on OpenVSX, if only key exists, set its entry to true instead of the original object 80 | const exists = await Promise.all(extensionsToCheck.map(existsOnOpenVSX)); 81 | if (exists.every((x) => x)) { 82 | updatedData.deprecated[key] = latestJson.deprecated[key]; 83 | } 84 | 85 | if (exists.length === 2 && exists[0] && !exists[1]) { 86 | missingDependency.push(key); 87 | } 88 | } 89 | 90 | for (const key of Object.keys(latestJson.migrateToPreRelease)) { 91 | if (key in localData.migrateToPreRelease) { 92 | continue; 93 | } 94 | 95 | const exists = await existsOnOpenVSX(key); 96 | if (exists) { 97 | updatedData.migrateToPreRelease[key] = latestJson.migrateToPreRelease[key]; 98 | } 99 | } 100 | 101 | for (const value of Object.values(latestJson.search)) { 102 | if (value.query && value.preferredResults) { 103 | const localValue = localData?.search?.find((x) => x.query === value.query); 104 | if (!localValue) { 105 | const newEntry = { query: value.query, preferredResults: [] }; 106 | for (const entry of value.preferredResults) { 107 | const exists = await existsOnOpenVSX(entry); 108 | if (exists) { 109 | newEntry.preferredResults.push(entry); 110 | } 111 | } 112 | 113 | if (newEntry.preferredResults.length > 0) { 114 | updatedData.search.push(newEntry); 115 | } 116 | continue; 117 | } 118 | } 119 | } 120 | 121 | for (const key of Object.values(latestJson.extensionsEnabledWithPreRelease)) { 122 | if (!localData.extensionsEnabledWithPreRelease.includes(key)) { 123 | const exists = await existsOnOpenVSX(key); 124 | if (exists) { 125 | updatedData.extensionsEnabledWithPreRelease.push(key); 126 | } 127 | } 128 | } 129 | 130 | const totalNumberBefore = Object.keys(localData) 131 | .map((x) => Object.keys(localData[x]).length) 132 | .reduce((a, b) => a + b, 0); 133 | 134 | const totalNumberAfter = Object.keys(updatedData) 135 | .map((x) => Object.keys(updatedData[x]).length) 136 | .reduce((a, b) => a + b, 0); 137 | 138 | console.log(`Total number of entries before: ${totalNumberBefore}`); 139 | console.log(`Total number of entries after: ${totalNumberAfter}`); 140 | console.log("Missing dependencies:", missingDependency); 141 | 142 | console.log(diff(updatedData, localData)); 143 | 144 | Bun.write(localFile, JSON.stringify(updatedData, null, 4)); 145 | -------------------------------------------------------------------------------- /extensions-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "$schema": { 5 | "type": "string" 6 | } 7 | }, 8 | "additionalProperties": { 9 | "type": "object", 10 | "properties": { 11 | "repository": { 12 | "type": "string", 13 | "description": "Repository URL to clone and publish from. If the extension publishes `.vsix` files as release artifacts, this will determine the repo to fetch the releases from." 14 | }, 15 | "location": { 16 | "type": "string", 17 | "description": "Location of the extension's package.json in the repository (defaults to the repository's root directory)" 18 | }, 19 | "prepublish": { 20 | "type": "string", 21 | "description": "Extra commands to run just before publishing to Open VSX (i.e. after 'yarn/npm install', but before 'vscode:prepublish')" 22 | }, 23 | "extensionFile": { 24 | "type": "string", 25 | "description": "Relative path of the extension vsix file inside the git repo (i.e. when it is built by prepublish commands" 26 | }, 27 | "custom": { 28 | "type": "array", 29 | "description": "Build using a custom script. Must output an `extension.vsix` file in the `location` directory (can be changed using `extensionFile`)." 30 | }, 31 | "timeout": { 32 | "type": "number", 33 | "description": "Timeout to build the extension vsix from sources." 34 | }, 35 | "pythonVersion": { 36 | "type": "string", 37 | "description": "Python version to use with this build." 38 | }, 39 | "msMarketplaceIdOverride": { 40 | "type": "string", 41 | "description": "A property to set a different lookup ID when querying the Microsoft Marketplace. Please do not ever use if not absolutely necessary." 42 | }, 43 | "target": { 44 | "type": "object", 45 | "description": "An object containing the ids of platforms to target while publishing. If unspecified, a universal extension will be published in case of building from source and if the vsix is resolved from GitHub Releases, all of the attached platform-specific assets will be published. The value of the key should be either `true` or an object specifying environment variables to be applied while packaging inside of `env`." 46 | } 47 | }, 48 | "required": ["repository"], 49 | "additionalProperties": false 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | artifactDirectory: "/tmp/artifacts", 3 | registryHost: "open-vsx.org", 4 | repoPath: "/tmp/repository/main", 5 | defaultPythonVersion: "3.8", 6 | }; 7 | -------------------------------------------------------------------------------- /lib/exec.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2020 TypeFox and others 3 | * 4 | * This program and the accompanying materials are made available under the 5 | * terms of the Eclipse Public License v. 2.0 which is available at 6 | * http://www.eclipse.org/legal/epl-2.0. 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | ********************************************************************************/ 10 | 11 | // @ts-check 12 | const cp = require("child_process"); 13 | 14 | /** 15 | * @param {string} command 16 | * @param {{cwd?: string, quiet?: boolean, ghtoken?: boolean}} [options] 17 | * @returns {Promise<{ stdout: string, stderr: string }>} 18 | */ 19 | module.exports = async (command, options) => { 20 | if (!options?.quiet) { 21 | console.log(`Running: ${command}`); 22 | } 23 | return new Promise((resolve, reject) => { 24 | const child = cp.exec( 25 | command, 26 | { 27 | cwd: options?.cwd, 28 | maxBuffer: 10 * 1024 * 1024, // 10MB 29 | env: { 30 | ...process.env, 31 | // remove on purpose to work around issues in vscode package 32 | GITHUB_TOKEN: options?.ghtoken ? process.env.GITHUB_TOKEN : undefined, 33 | }, 34 | shell: "/bin/bash", 35 | }, 36 | (error, stdout, stderr) => { 37 | if (error) { 38 | return reject(error); 39 | } 40 | resolve({ stdout, stderr }); 41 | }, 42 | ); 43 | if (!options?.quiet) { 44 | child.stdout.pipe(process.stdout); 45 | } 46 | child.stderr.pipe(process.stderr); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /lib/helpers.ts: -------------------------------------------------------------------------------- 1 | import { PublishStat } from "../types"; 2 | import { registryHost } from "./constants"; 3 | 4 | export const lineBreak = "\r\n"; 5 | 6 | export const positionOf = (item: any, array: any[]): string => `${array.indexOf(item) + 1}.`; 7 | 8 | export const generateMicrosoftLink = (id: string) => 9 | `[${id}](https://marketplace.visualstudio.com/items?itemName=${id})`; 10 | 11 | export const generateOpenVsxLink = (id: string) => 12 | `[${id}](https://${registryHost}/extension/${id.split(".")[0]}/${id.split(".")[1]})`; 13 | 14 | export const calculatePercentage = (value: number, total: number): string => `${((value / total) * 100).toFixed(0)}%`; 15 | 16 | export const readPublishStatistics = async (): Promise => { 17 | const file = Bun.file("/tmp/stat.json"); 18 | return await file.json(); 19 | }; 20 | -------------------------------------------------------------------------------- /lib/reportStat.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2022 TypeFox and others 3 | * 4 | * This program and the accompanying materials are made available under the 5 | * terms of the Eclipse Public License v. 2.0 which is available at 6 | * http://www.eclipse.org/legal/epl-2.0. 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | ********************************************************************************/ 10 | 11 | // @ts-check 12 | const fs = require("fs"); 13 | const { getPublicGalleryAPI } = require("@vscode/vsce/out/util"); 14 | const { PublicGalleryAPI } = require("@vscode/vsce/out/publicgalleryapi"); 15 | const { ExtensionQueryFlags } = require("azure-devops-node-api/interfaces/GalleryInterfaces"); 16 | const humanNumber = require("human-number"); 17 | const { registryHost } = require("./constants"); 18 | 19 | const formatter = (/** @type {number} */ number) => { 20 | if (number === undefined) { 21 | return "N/A"; 22 | } 23 | 24 | if (isNaN(number)) { 25 | return number.toString(); 26 | } 27 | 28 | const maxDigits = 3; 29 | let formatted = number.toFixed(maxDigits); 30 | while (formatted.endsWith("0")) { 31 | formatted = formatted.slice(0, -1); 32 | } 33 | return formatted; 34 | }; 35 | 36 | const msGalleryApi = getPublicGalleryAPI(); 37 | msGalleryApi.client["_allowRetries"] = true; 38 | msGalleryApi.client["_maxRetries"] = 5; 39 | 40 | const openGalleryApi = new PublicGalleryAPI(`https://${registryHost}/vscode`, "3.0-preview.1"); 41 | openGalleryApi.client["_allowRetries"] = true; 42 | openGalleryApi.client["_maxRetries"] = 5; 43 | openGalleryApi.post = ( 44 | /** @type {string} */ url, 45 | /** @type {string} */ data, 46 | /** @type {import("typed-rest-client/Interfaces").IHeaders} */ additionalHeaders, 47 | ) => openGalleryApi.client.post(`${openGalleryApi.baseUrl}${url}`, data, additionalHeaders); 48 | 49 | const flags = [ExtensionQueryFlags.IncludeStatistics]; 50 | 51 | const checkAmount = 128; 52 | 53 | const cannotPublish = [ 54 | // We cannot redistribute under their license: https://github.com/microsoft/vscode-cpptools/tree/main/RuntimeLicenses 55 | "ms-vscode.cpptools", 56 | "ms-vscode.cpptools-extension-pack", 57 | 58 | // We cannot redistribute under their license: https://github.com/OmniSharp/omnisharp-vscode/tree/master/RuntimeLicenses 59 | "ms-dotnettools.csharp", 60 | 61 | // Dependent on ms-dotnettools.csharp 62 | "vsciot-vscode.vscode-arduino", 63 | "Unity.unity-debug", 64 | "cake-build.cake-vscode", 65 | 66 | // Dependent on ms-vscode.cpptools 67 | "platformio.platformio-ide", 68 | 69 | // All code or parts are proprietary 70 | "ms-python.vscode-pylance", 71 | "ms-toolsai.vscode-ai-remote", 72 | "VisualStudioExptTeam.vscodeintellicode", 73 | "ms-dotnettools.vscodeintellicode-csharp", 74 | "VisualStudioExptTeam.intellicode-api-usage-examples", 75 | "ms-vscode-remote.remote-wsl", 76 | "ms-vscode-remote.remote-containers", 77 | "ms-vscode-remote.remote-ssh", 78 | "ms-vscode-remote.remote-ssh-edit", 79 | "ms-vscode.remote-explorer", 80 | "ms-vscode.remote-server", 81 | "ms-vscode.remote-repositories", 82 | "ms-vscode-remote.vscode-remote-extensionpack", 83 | "MS-vsliveshare.vsliveshare", 84 | "MS-vsliveshare.vsliveshare-pack", 85 | "MS-vsliveshare.vsliveshare-audio", 86 | "ms-python.gather", 87 | "ms-dotnettools.csdevkit", 88 | "ms-toolsai.vscode-ai", 89 | 90 | // GitHub Proprietary 91 | "GitHub.copilot", 92 | "GitHub.copilot-chat", 93 | "GitHub.remotehub", 94 | "GitHub.codespaces", 95 | 96 | // Deprecated 97 | "eg2.tslint", 98 | 99 | // Unlicensed 100 | "austin.code-gnu-global", 101 | 102 | // Other 103 | "humao.rest-client", // As per author's request: https://github.com/Huachao/vscode-restclient/issues/860#issuecomment-873097908 104 | ]; 105 | 106 | /** 107 | * Checks missing extensions from Open VSX 108 | * @param {boolean} silent 109 | * @param {number} amount 110 | * @returns 111 | */ 112 | const checkMissing = async (silent = false, amount = checkAmount) => { 113 | /** 114 | * @type {Readonly} 115 | */ 116 | const extensionsToPublish = JSON.parse(await fs.promises.readFile("./extensions.json", "utf-8")); 117 | 118 | /** @type {import('../types').SingleExtensionQueryResult[]} */ 119 | const topExtensions = await msGalleryApi.extensionQuery({ 120 | pageSize: amount, 121 | criteria: [ 122 | { filterType: 8, value: "Microsoft.VisualStudio.Code" }, 123 | { filterType: 12, value: "4096" }, 124 | ], 125 | flags, 126 | }); 127 | 128 | /** @type {import('../types').SingleExtensionQueryResult[]} */ 129 | let notInOvsx = []; 130 | 131 | const installs = { 132 | published: 0, 133 | missing: 0, 134 | }; 135 | 136 | for (const extension of topExtensions) { 137 | let [openExtension] = await Promise.allSettled([ 138 | openGalleryApi.getExtension(`${extension.publisher.publisherName}.${extension.extensionName}`, flags), 139 | ]); 140 | const id = `${extension.publisher.publisherName}.${extension.extensionName}`; 141 | const extInstalls = extension.statistics?.find((s) => s.statisticName === "install")?.value ?? 0; 142 | const popularExtension = extInstalls > 1_000_000; 143 | if (openExtension.status === "fulfilled") { 144 | if (!openExtension.value?.publisher.publisherId) { 145 | notInOvsx.push(extension); 146 | installs.missing += extInstalls; 147 | if (!silent) { 148 | if (!cannotPublish.includes(id)) { 149 | console.log( 150 | `${popularExtension ? "🔴" : "🟡"} Extension not in Open VSX : ${extension.publisher.publisherName}.${extension.extensionName}`, 151 | ); 152 | } 153 | } 154 | } else { 155 | installs.published += extInstalls; 156 | } 157 | } 158 | } 159 | 160 | const microsoftUnpublished = notInOvsx.filter( 161 | (extension) => 162 | ["https://microsoft.com", "https://github.com"].includes(extension.publisher.domain) && 163 | extension.publisher.isDomainVerified, 164 | ); 165 | const definedInRepo = notInOvsx 166 | .map((ext) => `${ext.publisher.publisherName}.${ext.extensionName}`) 167 | .filter((id) => extensionsToPublish[id]); 168 | 169 | const microsoftCouldPublish = microsoftUnpublished.filter( 170 | (extension) => !cannotPublish.includes(`${extension.publisher.publisherName}.${extension.extensionName}`), 171 | ); 172 | 173 | let summary = "----- Summary -----\r\n"; 174 | summary += `Total: ${amount}\r\n`; 175 | summary += `Install parity: ${((installs.published / (installs.missing + installs.published)) * 100).toFixed(2)}% (${humanNumber(installs.published, formatter)} out of ${humanNumber(installs.missing + installs.published, formatter)})\r\n`; 176 | summary += `Not published to Open VSX: ${notInOvsx.length} (${((notInOvsx.length / amount) * 100).toFixed(4)}%)\r\n`; 177 | summary += `Not in Open VSX but defined in our repo: ${definedInRepo.length} (${((definedInRepo.length / notInOvsx.length) * 100).toFixed(4)}%)\r\n`; 178 | summary += `Not published from Microsoft: ${microsoftUnpublished.length} (${((microsoftUnpublished.length / amount) * 100).toFixed(4)}% of all unpublished)\r\n`; 179 | 180 | summary += "Microsoft extensions we should publish: \r\n"; 181 | summary += microsoftCouldPublish 182 | .map((extension) => `${extension.publisher.publisherName}.${extension.extensionName}`) 183 | .join(); 184 | 185 | if (!silent) console.log(summary); 186 | 187 | return { 188 | missing: notInOvsx, 189 | missingMs: microsoftUnpublished, 190 | couldPublishMs: microsoftCouldPublish, 191 | definedInRepo, 192 | }; 193 | }; 194 | 195 | // Can also be run directly 196 | if (require.main === module) { 197 | (async function () { 198 | await checkMissing(false, 4096); 199 | })(); 200 | } 201 | 202 | exports.checkMissing = checkMissing; 203 | exports.formatter = formatter; 204 | exports.cannotPublish = cannotPublish; 205 | -------------------------------------------------------------------------------- /lib/resolveExtension.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const Octokit = require("octokit").Octokit; 5 | const readVSIXPackage = require("@vscode/vsce/out/zip").readVSIXPackage; 6 | const download = require("download"); 7 | const exec = require("./exec"); 8 | const { repoPath } = require("./constants"); 9 | 10 | const token = process.env.GITHUB_TOKEN; 11 | if (!token) { 12 | console.warn("GITHUB_TOKEN env var is not set. Skipping lookup from releases"); 13 | } 14 | const octokit = new Octokit({ auth: token }); 15 | 16 | /** 17 | * 18 | * @param {Readonly} extension 19 | * @param {{version: string, lastUpdated: Date} | undefined} [ms] 20 | * @returns {Promise} 21 | */ 22 | exports.resolveExtension = async function ({ id, repository, location }, ms) { 23 | if (!repository) throw TypeError("repository URL not supplied"); 24 | 25 | const repositoryUrl = new URL(repository); 26 | const [owner, repo] = repositoryUrl.pathname.slice(1).split("/"); 27 | 28 | //#region check latest release assets 29 | /** @type {string | undefined} */ 30 | let releaseTag; 31 | if (ms && repositoryUrl.hostname === "github.com" && token) { 32 | try { 33 | const releaseResponse = await octokit.rest.repos.getLatestRelease({ owner, repo }); 34 | const release = releaseResponse.data; 35 | releaseTag = release.tag_name; 36 | 37 | const releaseAssets = release.assets 38 | .map((asset) => asset.browser_download_url) 39 | .filter((downloadURL) => downloadURL.match(/\/releases\/download\/[-._a-zA-Z0-9\/%]*\.vsix$/g)); 40 | 41 | /** @type {{[key: string]: string}} */ 42 | const platformSpecific = {}; 43 | await exec("rm -rf /tmp/download", { quiet: true }); 44 | 45 | for (const releaseAsset of releaseAssets) { 46 | const file = `/tmp/download/${path.basename(releaseAsset)}`; 47 | console.info(`Downloading ${releaseAsset} to ${file}`); 48 | await download(releaseAsset, path.dirname(file), { filename: path.basename(file) }); 49 | const { manifest, xmlManifest } = await readVSIXPackage(file); 50 | const targetPlatform = xmlManifest?.PackageManifest?.Metadata[0]?.Identity[0]?.$?.TargetPlatform; 51 | 52 | if ( 53 | manifest.version === ms.version && 54 | `${manifest.publisher}.${manifest.name}`.toLowerCase() === id.toLowerCase() 55 | ) { 56 | if (targetPlatform) { 57 | platformSpecific[targetPlatform] = file; 58 | } else { 59 | // Don't overwrite `universal` if there is a file attached already 60 | if (!platformSpecific.universal) { 61 | platformSpecific.universal = file; 62 | } 63 | } 64 | } 65 | } 66 | 67 | if (Object.keys(platformSpecific).length > 0) { 68 | return { 69 | version: ms.version, 70 | files: platformSpecific, 71 | path: "", 72 | resolution: { releaseAsset: "resolved" }, 73 | }; 74 | } 75 | } catch {} 76 | } 77 | //#endregion 78 | 79 | await exec(`git clone --filter=blob:none --recurse-submodules ${repository} ${repoPath}`, { quiet: true }); 80 | 81 | const packagePath = [repoPath, location, "package.json"].filter((p) => !!p).join("/"); 82 | /** 83 | * @param {string} ref 84 | * @returns {Promise} 85 | */ 86 | async function resolveVersion(ref) { 87 | try { 88 | await exec(`git reset --hard ${ref} --quiet`, { cwd: repoPath, quiet: true }); 89 | const manifest = JSON.parse(await fs.promises.readFile(packagePath, "utf-8")); 90 | if (`${manifest.publisher}.${manifest.name}`.toLowerCase() !== id.toLowerCase()) { 91 | return undefined; 92 | } 93 | return manifest.version; 94 | } catch { 95 | return undefined; 96 | } 97 | } 98 | 99 | const latestCommit = (await exec(`git log -1 --oneline --format="%H %cD"`, { cwd: repoPath, quiet: true })).stdout 100 | .split("\n") 101 | .map((r) => r.trim()) 102 | .filter((r) => !!r) 103 | .map((r) => { 104 | const index = r.indexOf(" "); 105 | const sha = r.substring(0, index); 106 | const date = r.substring(index); 107 | return { sha, date: new Date(date) }; 108 | })[0]; 109 | 110 | /** @type {undefined | {sha: string, date: Date}[]} */ 111 | let matchedCommits; 112 | if (ms) { 113 | const until = new Date(ms.lastUpdated.getTime() + 12 * (60 * 60 * 1000)); 114 | until.setUTCHours(23, 59, 59, 999); 115 | matchedCommits = ( 116 | await exec(`git log -30 --oneline --format="%H %cD" --until="${until.toISOString()}"`, { 117 | cwd: repoPath, 118 | quiet: true, 119 | }) 120 | ).stdout 121 | .split("\n") 122 | .map((r) => r.trim()) 123 | .filter((r) => !!r) 124 | .map((r) => { 125 | const index = r.indexOf(" "); 126 | const sha = r.substring(0, index); 127 | const date = r.substring(index); 128 | return { sha, date: new Date(date) }; 129 | }); 130 | } 131 | 132 | // check latest release tag 133 | if (ms && releaseTag) { 134 | const version = await resolveVersion(releaseTag); 135 | if (version && ms.version.includes(version)) { 136 | return { version: ms.version, path: repoPath, resolution: { releaseTag } }; 137 | } 138 | } 139 | 140 | const releaseTags = ( 141 | await exec(`git log -3 --no-walk --tags --oneline --format="%H"`, { cwd: repoPath, quiet: true }) 142 | ).stdout 143 | .split("\n") 144 | .map((t) => t.trim()) 145 | .filter((t) => !!t); 146 | for (const tag of releaseTags) { 147 | const versionAtTag = await resolveVersion(tag); 148 | if (!versionAtTag) { 149 | continue; 150 | } 151 | if (ms) { 152 | if (ms.version.includes(versionAtTag)) { 153 | return { version: ms.version, path: repoPath, resolution: { tag } }; 154 | } 155 | } else { 156 | return { version: versionAtTag, path: repoPath, resolution: { tag } }; 157 | } 158 | } 159 | 160 | // if latest commit is too old like 3 months old then we just use last commit 161 | if (!ms) { 162 | if (!latestCommit) { 163 | return undefined; 164 | } 165 | const version = await resolveVersion(latestCommit.sha); 166 | if (!version) { 167 | return undefined; 168 | } 169 | return { version, path: repoPath, resolution: { latest: latestCommit.sha } }; 170 | } 171 | 172 | if (latestCommit) { 173 | const longTimeAgo = new Date(); 174 | longTimeAgo.setMonth(longTimeAgo.getMonth() - 2); 175 | if (longTimeAgo.getTime() > latestCommit.date.getTime()) { 176 | const version = await resolveVersion(latestCommit.sha); 177 | if (version) { 178 | return { version, path: repoPath, resolution: { latest: latestCommit.sha } }; 179 | } 180 | } 181 | } 182 | 183 | // match commit around last updated date 184 | let latestMatched; 185 | if (matchedCommits) { 186 | for (const [index, commit] of matchedCommits.entries()) { 187 | const ref = commit.sha; 188 | const version = await resolveVersion(ref); 189 | if (index === 0 && version) { 190 | latestMatched = { version, path: repoPath, resolution: { matchedLatest: ref } }; 191 | // if it is the latest commit then just use it 192 | if (ref === latestCommit.sha) { 193 | return latestMatched; 194 | } 195 | } 196 | if (version && ms.version.includes(version)) { 197 | return { version: ms.version, path: repoPath, resolution: { matched: ref } }; 198 | } 199 | } 200 | } 201 | return latestMatched; 202 | }; 203 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "publish-to-open-vsx", 3 | "version": "0.0.0", 4 | "description": "CI for publishing VS Code extensions to open-vsx.org", 5 | "homepage": "https://open-vsx.org", 6 | "license": "EPL-2.0", 7 | "bugs": { 8 | "url": "https://github.com/open-vsx/publish-extensions/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/open-vsx/publish-extensions" 13 | }, 14 | "contributors": [ 15 | { 16 | "name": "TypeFox", 17 | "url": "https://www.typefox.io" 18 | }, 19 | { 20 | "name": "Gitpod", 21 | "url": "https://www.gitpod.io" 22 | } 23 | ], 24 | "bin": "bin/publish-extensions", 25 | "main": "publish-extensions", 26 | "scripts": { 27 | "publish": "node publish-extensions", 28 | "format": "prettier --write ." 29 | }, 30 | "dependencies": { 31 | "@vscode/vsce": "^3.0.0", 32 | "ajv": "^8.8.1", 33 | "chai": "^5.1.0", 34 | "deep-equal-in-any-order": "^2.0.6", 35 | "download": "^8.0.0", 36 | "fast-glob": "^3.2.12", 37 | "find-up": "^5.0.0", 38 | "human-number": "^2.0.0", 39 | "jest-diff": "^29.7.0", 40 | "minimist": "^1.2.5", 41 | "octokit": "^3.1.2", 42 | "ovsx": "latest", 43 | "prettier": "^3.2.5", 44 | "semver": "^7.1.3" 45 | }, 46 | "devDependencies": { 47 | "@types/human-number": "^1.0.0", 48 | "@types/node": "^22.2.0", 49 | "@types/unzipper": "^0.10.5", 50 | "bun-types": "^1.0.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /publish-extension.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2023 TypeFox and others 3 | * 4 | * This program and the accompanying materials are made available under the 5 | * terms of the Eclipse Public License v. 2.0 which is available at 6 | * http://www.eclipse.org/legal/epl-2.0. 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | ********************************************************************************/ 10 | 11 | // @ts-check 12 | const fs = require("fs"); 13 | const ovsx = require("ovsx"); 14 | const readVSIXPackage = require("@vscode/vsce/out/zip").readVSIXPackage; 15 | const path = require("path"); 16 | const semver = require("semver"); 17 | const exec = require("./lib/exec"); 18 | const findUp = require("find-up"); 19 | const fg = require("fast-glob"); 20 | 21 | const { createVSIX } = require("@vscode/vsce"); 22 | const { cannotPublish } = require("./lib/reportStat"); 23 | 24 | const { PublicGalleryAPI } = require("@vscode/vsce/out/publicgalleryapi"); 25 | const { PublishedExtension } = require("azure-devops-node-api/interfaces/GalleryInterfaces"); 26 | const { artifactDirectory, registryHost, defaultPythonVersion } = require("./lib/constants"); 27 | 28 | const vscodeBuiltinExtensionsNamespace = "vscode"; 29 | const isBuiltIn = (id) => id.split(".")[0] === vscodeBuiltinExtensionsNamespace; 30 | 31 | const openGalleryApi = new PublicGalleryAPI(`https://${registryHost}/vscode`, "3.0-preview.1"); 32 | openGalleryApi.client["_allowRetries"] = true; 33 | openGalleryApi.client["_maxRetries"] = 5; 34 | openGalleryApi.post = (url, data, additionalHeaders) => 35 | openGalleryApi.client.post(`${openGalleryApi.baseUrl}${url}`, data, additionalHeaders); 36 | 37 | (async () => { 38 | /** 39 | * @type {{extension: import('./types').Extension, context: import('./types').PublishContext, extensions: Readonly}} 40 | */ 41 | const { extension, context, extensions } = JSON.parse(process.argv[2]); 42 | console.log(`\nProcessing extension: ${JSON.stringify({ extension, context }, undefined, 2)}`); 43 | try { 44 | const { id } = extension; 45 | const [namespace] = id.split("."); 46 | 47 | let packagePath = context.repo; 48 | if (packagePath && extension.location) { 49 | packagePath = path.join(packagePath, extension.location); 50 | } 51 | 52 | /** @type {import('ovsx').PublishOptions} */ 53 | let options; 54 | if (context.file) { 55 | options = { extensionFile: context.file, targets: [context.target] }; 56 | } else if (context.repo && context.ref) { 57 | console.log(`${id}: preparing from ${context.repo}...`); 58 | 59 | const [publisher, name] = extension.id.split("."); 60 | process.env.EXTENSION_ID = extension.id; 61 | process.env.EXTENSION_PUBLISHER = publisher; 62 | process.env.EXTENSION_NAME = name; 63 | process.env.VERSION = context.version; 64 | process.env.MS_VERSION = context.msVersion; 65 | process.env.OVSX_VERSION = context.ovsxVersion; 66 | await exec(`git checkout ${context.ref}`, { cwd: context.repo }); 67 | 68 | try { 69 | const nvmFile = await findUp(".nvmrc", { cwd: path.join(context.repo, extension.location ?? ".") }); 70 | if (nvmFile) { 71 | // If the project has a preferred Node version, use it 72 | await exec("source ~/.nvm/nvm.sh && nvm install", { 73 | cwd: path.join(context.repo, extension.location ?? "."), 74 | quiet: true, 75 | }); 76 | } 77 | 78 | if (extension.pythonVersion) { 79 | console.debug("Installing appropriate Python version..."); 80 | await exec( 81 | `pyenv install -s ${extension.pythonVersion} && pyenv global ${extension.pythonVersion}`, 82 | { cwd: path.join(context.repo, extension.location ?? "."), quiet: false }, 83 | ); 84 | } 85 | } catch {} 86 | 87 | if (extension.custom) { 88 | try { 89 | for (const command of extension.custom) { 90 | await exec(command, { cwd: context.repo }); 91 | } 92 | 93 | options = { 94 | extensionFile: path.join( 95 | context.repo, 96 | extension.location ?? ".", 97 | extension.extensionFile ?? "extension.vsix", 98 | ), 99 | }; 100 | 101 | if (context.target) { 102 | console.info(`Looking for a ${context.target} vsix package in ${context.repo}...`); 103 | const vsixFiles = await fg(path.join(`*-${context.target}-*.vsix`), { 104 | cwd: context.repo, 105 | onlyFiles: true, 106 | }); 107 | if (vsixFiles.length > 0) { 108 | console.info( 109 | `Found ${vsixFiles.length} ${context.target} vsix package(s) in ${context.repo}: ${vsixFiles.join(", ")}`, 110 | ); 111 | options = { 112 | extensionFile: path.join(context.repo, vsixFiles[0]), 113 | targets: [context.target], 114 | }; 115 | } else { 116 | throw new Error( 117 | `After running the custom commands, no .vsix file was found for ${extension.id}@${context.target}`, 118 | ); 119 | } 120 | } 121 | } catch (e) { 122 | throw e; 123 | } 124 | } else { 125 | const yarn = await new Promise((resolve) => { 126 | fs.access(path.join(context.repo, "yarn.lock"), (error) => resolve(!error)); 127 | }); 128 | try { 129 | await exec(`${yarn ? "yarn" : "npm"} install`, { cwd: packagePath }); 130 | } catch (e) { 131 | const pck = JSON.parse(await fs.promises.readFile(path.join(packagePath, "package.json"), "utf-8")); 132 | // try to auto migrate from vscode: https://code.visualstudio.com/api/working-with-extensions/testing-extension#migrating-from-vscode 133 | if (pck.scripts?.postinstall === "node ./node_modules/vscode/bin/install") { 134 | delete pck.scripts["postinstall"]; 135 | pck.devDependencies = pck.devDependencies || {}; 136 | delete pck.devDependencies["vscode"]; 137 | pck.devDependencies["@types/vscode"] = pck.engines["vscode"]; 138 | const content = JSON.stringify(pck, undefined, 2).replace( 139 | /node \.\/node_modules\/vscode\/bin\/compile/g, 140 | "tsc", 141 | ); 142 | await fs.promises.writeFile(path.join(packagePath, "package.json"), content, "utf-8"); 143 | await exec(`${yarn ? "yarn" : "npm"} install`, { cwd: packagePath }); 144 | } else { 145 | throw e; 146 | } 147 | } 148 | if (extension.prepublish) { 149 | await exec(extension.prepublish, { cwd: context.repo }); 150 | } 151 | if (extension.extensionFile) { 152 | options = { extensionFile: path.join(context.repo, extension.extensionFile) }; 153 | } else { 154 | options = { extensionFile: path.join(context.repo, "extension.vsix") }; 155 | if (yarn) { 156 | options.yarn = true; 157 | } 158 | // answer y to all questions https://github.com/microsoft/vscode-vsce/blob/7182692b0f257dc10e7fc643269511549ca0c1db/src/util.ts#L12 159 | const vsceTests = process.env["VSCE_TESTS"]; 160 | process.env["VSCE_TESTS"] = "1"; 161 | try { 162 | await createVSIX({ 163 | cwd: packagePath, 164 | packagePath: options.extensionFile, 165 | baseContentUrl: options.baseContentUrl, 166 | baseImagesUrl: options.baseImagesUrl, 167 | useYarn: options.yarn, 168 | target: context.target, 169 | }); 170 | } finally { 171 | process.env["VSCE_TESTS"] = vsceTests; 172 | } 173 | } 174 | console.log(`${id}: prepared from ${context.repo}`); 175 | } 176 | } 177 | 178 | // Check if the requested version is greater than the one on Open VSX. 179 | const { xmlManifest, manifest } = options.extensionFile && (await readVSIXPackage(options.extensionFile)); 180 | context.version = xmlManifest?.PackageManifest?.Metadata[0]?.Identity[0]["$"]?.Version || manifest?.version; 181 | if (!context.version) { 182 | throw new Error(`${extension.id}: version is not resolved`); 183 | } 184 | 185 | if (context.ovsxVersion) { 186 | if (semver.gt(context.ovsxVersion, context.version)) { 187 | throw new Error( 188 | `extensions.json is out-of-date: Open VSX version ${context.ovsxVersion} is already greater than specified version ${context.version}`, 189 | ); 190 | } 191 | if (semver.eq(context.ovsxVersion, context.version) && process.env.FORCE !== "true") { 192 | console.log(`[SKIPPED] Requested version ${context.version} is already published on Open VSX`); 193 | return; 194 | } 195 | } 196 | 197 | // TODO(ak) check license is open-source 198 | if ( 199 | !xmlManifest?.PackageManifest?.Metadata[0]?.License?.[0] && 200 | !manifest.license && 201 | !(packagePath && (await ovsx.isLicenseOk(packagePath, manifest))) 202 | ) { 203 | throw new Error(`${extension.id}: license is missing`); 204 | } 205 | 206 | const { extensionDependencies } = manifest; 207 | if (extensionDependencies) { 208 | const extensionDependenciesNotBuiltin = extensionDependencies.filter((id) => !isBuiltIn(id)); 209 | const unpublishableDependencies = extensionDependenciesNotBuiltin.filter((dependency) => 210 | cannotPublish.includes(dependency), 211 | ); 212 | if (unpublishableDependencies?.length > 0) { 213 | throw new Error( 214 | `${id} is dependent on ${unpublishableDependencies.join(", ")}, which ${unpublishableDependencies.length === 1 ? "has" : "have"} to be published to Open VSX first by ${unpublishableDependencies.length === 1 ? "its author because of its license" : "their authors because of their licenses"}.`, 215 | ); 216 | } 217 | 218 | const dependenciesNotOnOpenVsx = []; 219 | for (const dependency of extensionDependenciesNotBuiltin) { 220 | if (process.env.SKIP_PUBLISH && Object.keys(extensions).find((key) => key === dependency)) { 221 | continue; 222 | } 223 | 224 | /** @type {[PromiseSettledResult]} */ 225 | const [ovsxExtension] = await Promise.allSettled([openGalleryApi.getExtension(dependency)]); 226 | if (ovsxExtension.status === "fulfilled" && !ovsxExtension.value) { 227 | dependenciesNotOnOpenVsx.push(dependency); 228 | } 229 | } 230 | if (dependenciesNotOnOpenVsx.length > 0) { 231 | throw new Error( 232 | `${id} is dependent on ${dependenciesNotOnOpenVsx.join(", ")}, which ${dependenciesNotOnOpenVsx.length === 1 ? "has" : "have"} to be published to Open VSX first`, 233 | ); 234 | } 235 | } 236 | 237 | if (options.extensionFile && process.env.EXTENSIONS) { 238 | console.info(`Copying file to ${artifactDirectory}`); 239 | const outputFile = `${extension.id}${context.target ? `@${context.target}` : ""}.vsix`; 240 | fs.copyFileSync(options.extensionFile, path.join("/tmp/artifacts/", outputFile)); 241 | } 242 | 243 | if (process.env.SKIP_PUBLISH === "true") { 244 | return; 245 | } 246 | 247 | console.log(`Attempting to publish ${id} to Open VSX`); 248 | 249 | // Create a public Open VSX namespace if needed. 250 | try { 251 | await ovsx.createNamespace({ name: namespace }); 252 | } catch (error) { 253 | console.log(`Creating Open VSX namespace failed -- assuming that it already exists`); 254 | console.log(error); 255 | } 256 | 257 | console.info(`Publishing extension as ${options.targets ? options.targets.join(", ") : "universal"}`); 258 | if (process.env.OVSX_PAT) { 259 | await ovsx.publish(options); 260 | console.log(`Published ${id} to https://${registryHost}/extension/${id.split(".")[0]}/${id.split(".")[1]}`); 261 | } else { 262 | console.error( 263 | "The OVSX_PAT environment variable was not provided, which means the extension cannot be published. Provide it or set SKIP_PUBLISH to true to avoid seeing this.", 264 | ); 265 | process.exitCode = 1; 266 | } 267 | } catch (error) { 268 | if (error && String(error).indexOf("is already published.") !== -1) { 269 | console.log(`Could not process extension -- assuming that it already exists`); 270 | console.log(error); 271 | } else { 272 | console.error(`[FAIL] Could not process extension: ${JSON.stringify({ extension, context }, null, 2)}`); 273 | console.error(error); 274 | process.exitCode = 1; 275 | } 276 | } finally { 277 | // Clean up 278 | if (extension.pythonVersion) { 279 | await exec(`pyenv global ${defaultPythonVersion}`); 280 | } 281 | } 282 | })(); 283 | -------------------------------------------------------------------------------- /publish-extensions.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2020 TypeFox and others 3 | * 4 | * This program and the accompanying materials are made available under the 5 | * terms of the Eclipse Public License v. 2.0 which is available at 6 | * http://www.eclipse.org/legal/epl-2.0. 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | ********************************************************************************/ 10 | 11 | // @ts-check 12 | const fs = require("fs"); 13 | const cp = require("child_process"); 14 | const { getPublicGalleryAPI } = require("@vscode/vsce/out/util"); 15 | const { PublicGalleryAPI } = require("@vscode/vsce/out/publicgalleryapi"); 16 | const { ExtensionQueryFlags, PublishedExtension } = require("azure-devops-node-api/interfaces/GalleryInterfaces"); 17 | const semver = require("semver"); 18 | const Ajv = require("ajv/dist/2020").default; 19 | const resolveExtension = require("./lib/resolveExtension").resolveExtension; 20 | const exec = require("./lib/exec"); 21 | const { artifactDirectory, registryHost } = require("./lib/constants"); 22 | 23 | const msGalleryApi = getPublicGalleryAPI(); 24 | msGalleryApi.client["_allowRetries"] = true; 25 | msGalleryApi.client["_maxRetries"] = 5; 26 | 27 | const openGalleryApi = new PublicGalleryAPI(`https://${registryHost}/vscode`, "3.0-preview.1"); 28 | openGalleryApi.client["_allowRetries"] = true; 29 | openGalleryApi.client["_maxRetries"] = 5; 30 | openGalleryApi.post = (url, data, additionalHeaders) => 31 | openGalleryApi.client.post(`${openGalleryApi.baseUrl}${url}`, data, additionalHeaders); 32 | 33 | const flags = [ 34 | ExtensionQueryFlags.IncludeStatistics, 35 | ExtensionQueryFlags.IncludeVersions, 36 | ExtensionQueryFlags.IncludeVersionProperties, 37 | ]; 38 | 39 | /** 40 | * Checks whether the provided `version` is a prerelease or not 41 | * @param {Readonly} version 42 | * @returns 43 | */ 44 | function isPreReleaseVersion(version) { 45 | const values = version ? version.filter((p) => p.key === "Microsoft.VisualStudio.Code.PreRelease") : []; 46 | return values.length > 0 && values[0].value === "true"; 47 | } 48 | 49 | const ensureBuildPrerequisites = async () => { 50 | // Make yarn use bash 51 | await exec("yarn config set script-shell /bin/bash"); 52 | 53 | // Don't show large git advice blocks 54 | await exec("git config --global advice.detachedHead false"); 55 | 56 | // Create directory for storing built extensions 57 | if (fs.existsSync(artifactDirectory)) { 58 | // If the folder has any files, delete them 59 | try { 60 | fs.rmSync(`${artifactDirectory}*`); 61 | } catch {} 62 | } else { 63 | fs.mkdirSync(artifactDirectory); 64 | } 65 | }; 66 | 67 | (async () => { 68 | await ensureBuildPrerequisites(); 69 | 70 | /** 71 | * @type {string[] | undefined} 72 | */ 73 | let toVerify = undefined; 74 | if (process.env.EXTENSIONS) { 75 | toVerify = process.env.EXTENSIONS === "," ? [] : process.env.EXTENSIONS.split(",").map((s) => s.trim()); 76 | } 77 | /** 78 | * @type {Readonly} 79 | */ 80 | const extensions = JSON.parse(await fs.promises.readFile("./extensions.json", "utf-8")); 81 | 82 | // Validate that extensions.json 83 | const JSONSchema = JSON.parse(await fs.promises.readFile("./extensions-schema.json", "utf-8")); 84 | 85 | const ajv = new Ajv(); 86 | const validate = ajv.compile(JSONSchema); 87 | const valid = validate(extensions); 88 | if (!valid) { 89 | console.error("extensions.json is invalid:"); 90 | console.error(validate.errors); 91 | process.exit(1); 92 | } 93 | 94 | // Also install extensions' devDependencies when using `npm install` or `yarn install`. 95 | process.env.NODE_ENV = "development"; 96 | 97 | /** @type{import('./types').PublishStat}*/ 98 | const stat = { 99 | upToDate: {}, 100 | outdated: {}, 101 | unstable: {}, 102 | notInOpen: {}, 103 | notInMS: [], 104 | failed: [], 105 | 106 | msPublished: {}, 107 | hitMiss: {}, 108 | resolutions: {}, 109 | }; 110 | const monthAgo = new Date(); 111 | monthAgo.setMonth(monthAgo.getMonth() - 1); 112 | for (const id in extensions) { 113 | if (id === "$schema") { 114 | continue; 115 | } 116 | if (toVerify && !toVerify.includes(id)) { 117 | continue; 118 | } 119 | const extension = Object.freeze({ id, ...extensions[id] }); 120 | /** @type {import('./types').PublishContext} */ 121 | const context = {}; 122 | let timeoutDelay = Number(extension.timeout); 123 | if (!Number.isInteger(timeoutDelay)) { 124 | timeoutDelay = 5; 125 | } 126 | try { 127 | const extensionId = extension.msMarketplaceIdOverride ?? extension.id; 128 | /** @type {[PromiseSettledResult]} */ 129 | let [msExtension] = await Promise.allSettled([msGalleryApi.getExtension(extensionId, flags)]); 130 | if (msExtension.status === "fulfilled") { 131 | const lastNonPrereleaseVersion = msExtension.value?.versions.find( 132 | (version) => !isPreReleaseVersion(version.properties), 133 | ); 134 | context.msVersion = lastNonPrereleaseVersion?.version; 135 | context.msLastUpdated = lastNonPrereleaseVersion?.lastUpdated; 136 | context.msInstalls = msExtension.value?.statistics?.find((s) => s.statisticName === "install")?.value; 137 | context.msPublisher = msExtension.value?.publisher.publisherName; 138 | } 139 | 140 | // Check if the extension is published by either Microsoft or GitHub 141 | if ( 142 | ["https://microsoft.com", "https://github.com"].includes(msExtension?.value?.publisher.domain) && 143 | msExtension?.value.publisher.isDomainVerified 144 | ) { 145 | stat.msPublished[extension.id] = { msInstalls: context.msInstalls, msVersion: context.msVersion }; 146 | } 147 | 148 | async function updateStat() { 149 | /** @type {[PromiseSettledResult]} */ 150 | const [ovsxExtension] = await Promise.allSettled([openGalleryApi.getExtension(extension.id, flags)]); 151 | if (ovsxExtension.status === "fulfilled") { 152 | context.ovsxVersion = ovsxExtension.value?.versions[0]?.version; 153 | context.ovsxLastUpdated = ovsxExtension.value?.versions[0]?.lastUpdated; 154 | } 155 | const daysInBetween = 156 | context.ovsxLastUpdated && context.msLastUpdated 157 | ? (context.ovsxLastUpdated.getTime() - context.msLastUpdated.getTime()) / (1000 * 3600 * 24) 158 | : undefined; 159 | const extStat = { 160 | msInstalls: context.msInstalls, 161 | msVersion: context.msVersion, 162 | openVersion: context.ovsxVersion, 163 | daysInBetween, 164 | }; 165 | 166 | const i = stat.notInMS.indexOf(extension.id); 167 | if (i !== -1) { 168 | stat.notInMS.splice(i, 1); 169 | } 170 | delete stat.notInOpen[extension.id]; 171 | delete stat.upToDate[extension.id]; 172 | delete stat.outdated[extension.id]; 173 | delete stat.unstable[extension.id]; 174 | delete stat.hitMiss[extension.id]; 175 | 176 | if (!context.msVersion) { 177 | stat.notInMS.push(extension.id); 178 | } else if (!context.ovsxVersion) { 179 | stat.notInOpen[extension.id] = extStat; 180 | } else if (semver.eq(context.msVersion, context.ovsxVersion)) { 181 | stat.upToDate[extension.id] = extStat; 182 | } else { 183 | // Some extensions have versioning which is a bit different, like for example in the format of 1.71.8240911. If this is the case and we don't have this version published, we do some more checking to get more context about this version string. 184 | const weirdVersionNumberPattern = new RegExp(/^\d{1,3}\.\d{1,}\.\d{4,}/g); // https://regexr.com/6t02m 185 | if (context.msVersion.match(weirdVersionNumberPattern)) { 186 | if ( 187 | `${semver.major(context.msVersion)}.${semver.minor(context.msVersion)}` === 188 | `${semver.major(context.ovsxVersion)}.${semver.minor(context.ovsxVersion)}` 189 | ) { 190 | // If major.minor are the same on both marketplaces, we assume we're up-to-date 191 | stat.upToDate[extension.id] = extStat; 192 | debugger; 193 | } else { 194 | stat.outdated[extension.id] = extStat; 195 | debugger; 196 | } 197 | } else { 198 | if (semver.gt(context.msVersion, context.ovsxVersion)) { 199 | stat.outdated[extension.id] = extStat; 200 | } else if (semver.lt(context.msVersion, context.ovsxVersion)) { 201 | stat.unstable[extension.id] = extStat; 202 | } 203 | } 204 | } 205 | 206 | if ( 207 | context.msVersion && 208 | context.msLastUpdated && 209 | monthAgo.getTime() <= context.msLastUpdated.getTime() 210 | ) { 211 | stat.hitMiss[extension.id] = extStat; 212 | } 213 | } 214 | 215 | await updateStat(); 216 | await exec("rm -rf /tmp/repository /tmp/download", { quiet: true }); 217 | 218 | const resolved = await resolveExtension( 219 | extension, 220 | context.msVersion && { 221 | version: context.msVersion, 222 | lastUpdated: context.msLastUpdated, 223 | }, 224 | ); 225 | stat.resolutions[extension.id] = { 226 | msInstalls: context.msInstalls, 227 | msVersion: context.msVersion, 228 | ...resolved?.resolution, 229 | }; 230 | context.version = resolved?.version; 231 | 232 | if (process.env.FORCE !== "true") { 233 | if (stat.upToDate[extension.id]) { 234 | console.log(`${extension.id}: skipping, since up-to-date`); 235 | continue; 236 | } 237 | if (stat.unstable[extension.id]) { 238 | console.log(`${extension.id}: skipping, since version in Open VSX is newer than in MS marketplace`); 239 | continue; 240 | } 241 | if (resolved?.resolution?.latest && context.version === context.ovsxVersion) { 242 | console.log(`${extension.id}: skipping, since very latest commit already published to Open VSX`); 243 | stat.upToDate[extension.id] = stat.outdated[extension.id]; 244 | delete stat.outdated[extension.id]; 245 | continue; 246 | } 247 | } 248 | 249 | if (resolved && !resolved?.resolution.releaseAsset) { 250 | context.repo = resolved.path; 251 | } 252 | 253 | if (resolved?.resolution?.releaseAsset) { 254 | console.log(`${extension.id}: resolved from release`); 255 | context.files = resolved.files; 256 | } else if (resolved?.resolution?.releaseTag) { 257 | console.log(`${extension.id}: resolved ${resolved.resolution.releaseTag} from release tag`); 258 | context.ref = resolved.resolution.releaseTag; 259 | } else if (resolved?.resolution?.tag) { 260 | console.log(`${extension.id}: resolved ${resolved.resolution.tag} from tags`); 261 | context.ref = resolved.resolution.tag; 262 | } else if (resolved?.resolution?.latest) { 263 | if (context.msVersion) { 264 | console.log( 265 | `${extension.id}: resolved ${resolved.resolution.latest} from the very latest commit, since it is not actively maintained`, 266 | ); 267 | } else { 268 | console.log( 269 | `${extension.id}: resolved ${resolved.resolution.latest} from the very latest commit, since it is not published to MS marketplace`, 270 | ); 271 | } 272 | context.ref = resolved.resolution.latest; 273 | } else if (resolved?.resolution?.matchedLatest) { 274 | console.log( 275 | `${extension.id}: resolved ${resolved.resolution.matchedLatest} from the very latest commit`, 276 | ); 277 | context.ref = resolved.resolution.matchedLatest; 278 | } else if (resolved?.resolution?.matched) { 279 | console.log( 280 | `${extension.id}: resolved ${resolved.resolution.matched} from the latest commit on the last update date`, 281 | ); 282 | context.ref = resolved.resolution.matched; 283 | } else { 284 | throw `${extension.id}: failed to resolve`; 285 | } 286 | 287 | if (process.env.SKIP_BUILD === "true") { 288 | continue; 289 | } 290 | 291 | let timeout; 292 | 293 | const publishVersion = async (extension, context) => { 294 | const env = { 295 | ...process.env, 296 | ...context.environmentVariables, 297 | }; 298 | 299 | console.debug(`Publishing ${extension.id} for ${context.target || "universal"}...`); 300 | 301 | await new Promise((resolve, reject) => { 302 | const p = cp.spawn( 303 | process.execPath, 304 | ["publish-extension.js", JSON.stringify({ extension, context, extensions })], 305 | { 306 | stdio: ["ignore", "inherit", "inherit"], 307 | cwd: process.cwd(), 308 | env, 309 | }, 310 | ); 311 | p.on("error", reject); 312 | p.on("exit", (code) => { 313 | if (code) { 314 | return reject(new Error("failed with exit status: " + code)); 315 | } 316 | resolve("done"); 317 | }); 318 | timeout = setTimeout( 319 | () => { 320 | try { 321 | p.kill("SIGKILL"); 322 | } catch {} 323 | reject(new Error(`timeout after ${timeoutDelay} mins`)); 324 | }, 325 | timeoutDelay * 60 * 1000, 326 | ); 327 | }); 328 | if (timeout !== undefined) { 329 | clearTimeout(timeout); 330 | } 331 | }; 332 | 333 | if (context.files) { 334 | // Publish all targets of extension from GitHub Release assets 335 | for (const [target, file] of Object.entries(context.files)) { 336 | if (!extension.target || Object.keys(extension.target).includes(target)) { 337 | context.file = file; 338 | context.target = target; 339 | await publishVersion(extension, context); 340 | } else { 341 | console.log(`${extension.id}: skipping, since target ${target} is not included`); 342 | } 343 | } 344 | } else if (extension.target) { 345 | // Publish all specified targets of extension from sources 346 | for (const [target, targetData] of Object.entries(extension.target)) { 347 | context.target = target; 348 | if (targetData !== true) { 349 | context.environmentVariables = targetData.env; 350 | } 351 | await publishVersion(extension, context); 352 | } 353 | } else { 354 | // Publish only the universal target of extension from sources 355 | await publishVersion(extension, context); 356 | } 357 | 358 | await updateStat(); 359 | } catch (error) { 360 | stat.failed.push(extension.id); 361 | console.error(`[FAIL] Could not process extension: ${JSON.stringify({ extension, context }, null, 2)}`); 362 | console.error(error); 363 | } 364 | } 365 | 366 | await fs.promises.writeFile("/tmp/stat.json", JSON.stringify(stat), { encoding: "utf8" }); 367 | process.exit(); 368 | })(); 369 | -------------------------------------------------------------------------------- /report-extensions.ts: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2021-2023 Gitpod and others 3 | * 4 | * This program and the accompanying materials are made available under the 5 | * terms of the Eclipse Public License v. 2.0 which is available at 6 | * http://www.eclipse.org/legal/epl-2.0. 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | ********************************************************************************/ 10 | 11 | import fs from "fs"; 12 | import humanNumber from "human-number"; 13 | 14 | import { 15 | calculatePercentage, 16 | generateMicrosoftLink, 17 | generateOpenVsxLink, 18 | lineBreak, 19 | positionOf, 20 | readPublishStatistics, 21 | } from "./lib/helpers"; 22 | import { formatter } from "./lib/reportStat"; 23 | import type { ExtensionStat, MSExtensionStat } from "./types"; 24 | 25 | type InputExtensionStat = Partial; 26 | function sortedKeys(s: { [id: string]: InputExtensionStat }) { 27 | return Object.keys(s).sort((a, b) => { 28 | const msInstallsA = s[a].msInstalls ?? 0; 29 | const msInstallsB = s[b].msInstalls ?? 0; 30 | 31 | return msInstallsB - msInstallsA; 32 | }); 33 | } 34 | 35 | const stat = await readPublishStatistics(); 36 | 37 | const getAggregatedInstalls = (category: "upToDate" | "unstable" | "outdated" | "notInOpen") => { 38 | return Object.keys(stat[category]) 39 | .map((st) => stat[category][st].msInstalls) 40 | .reduce((previousValue, currentValue) => previousValue + currentValue, 0); 41 | }; 42 | 43 | const aggregatedInstalls = { 44 | upToDate: getAggregatedInstalls("upToDate"), 45 | unstable: getAggregatedInstalls("unstable"), 46 | outdated: getAggregatedInstalls("outdated"), 47 | notInOpen: getAggregatedInstalls("notInOpen"), 48 | }; 49 | 50 | const upToDate = Object.keys(stat.upToDate).length; 51 | const unstable = Object.keys(stat.unstable).length; 52 | const outdated = Object.keys(stat.outdated).length; 53 | const notInOpen = Object.keys(stat.notInOpen).length; 54 | const notInMS = stat.notInMS.length; 55 | const total = upToDate + notInOpen + outdated + unstable + notInMS; 56 | const updatedInMTD = Object.keys(stat.hitMiss).length; 57 | const updatedInOpenIn2Days = new Set( 58 | Object.keys(stat.hitMiss).filter((id) => { 59 | const { daysInBetween } = stat.hitMiss[id]; 60 | return typeof daysInBetween === "number" && 0 <= Math.round(daysInBetween) && Math.round(daysInBetween) <= 2; 61 | }), 62 | ); 63 | const updatedInOpenIn2Weeks = new Set( 64 | Object.keys(stat.hitMiss).filter((id) => { 65 | const { daysInBetween } = stat.hitMiss[id]; 66 | return typeof daysInBetween === "number" && 0 <= Math.round(daysInBetween) && Math.round(daysInBetween) <= 14; 67 | }), 68 | ); 69 | const updatedInOpenInMonth = new Set( 70 | Object.keys(stat.hitMiss).filter((id) => { 71 | const { daysInBetween } = stat.hitMiss[id]; 72 | return typeof daysInBetween === "number" && 0 <= Math.round(daysInBetween) && Math.round(daysInBetween) <= 30; 73 | }), 74 | ); 75 | const msPublished = Object.keys(stat.msPublished).length; 76 | const msPublishedOutdated = Object.keys(stat.outdated).filter((id) => Object.keys(stat.msPublished).includes(id)); 77 | const msPublishedUnstable = Object.keys(stat.unstable).filter((id) => Object.keys(stat.msPublished).includes(id)); 78 | 79 | const totalResolutions = Object.keys(stat.resolutions).length; 80 | const fromReleaseAsset = Object.keys(stat.resolutions).filter((id) => stat.resolutions[id].releaseAsset).length; 81 | const fromReleaseTag = Object.keys(stat.resolutions).filter((id) => stat.resolutions[id].releaseTag).length; 82 | const fromTag = Object.keys(stat.resolutions).filter((id) => stat.resolutions[id].tag).length; 83 | const fromLatestUnmaintained = Object.keys(stat.resolutions).filter( 84 | (id) => stat.resolutions[id].latest && stat.resolutions[id].msVersion, 85 | ).length; 86 | const fromLatestNotPublished = Object.keys(stat.resolutions).filter( 87 | (id) => stat.resolutions[id].latest && !stat.resolutions[id].msVersion, 88 | ).length; 89 | const fromMatchedLatest = Object.keys(stat.resolutions).filter((id) => stat.resolutions[id].matchedLatest).length; 90 | const fromMatched = Object.keys(stat.resolutions).filter((id) => stat.resolutions[id].matched).length; 91 | const totalResolved = 92 | fromReleaseAsset + 93 | fromReleaseTag + 94 | fromTag + 95 | fromLatestUnmaintained + 96 | fromLatestNotPublished + 97 | fromMatchedLatest + 98 | fromMatched; 99 | 100 | const weightedPercentage = 101 | aggregatedInstalls.upToDate / 102 | (aggregatedInstalls.notInOpen + 103 | aggregatedInstalls.upToDate + 104 | aggregatedInstalls.outdated + 105 | aggregatedInstalls.unstable); 106 | 107 | let summary: string[] = ["# Summary"]; 108 | 109 | if (!process.env.EXTENSIONS) { 110 | summary.push( 111 | `Total: ${total}`, 112 | `Up-to-date (MS Marketplace == Open VSX): ${upToDate} (${calculatePercentage(upToDate, total)})`, 113 | `Weighted publish percentage: ${(weightedPercentage * 100).toFixed(0)}%`, 114 | `Outdated (Not in Open VSX, but in MS marketplace): ${notInOpen} (${calculatePercentage(notInOpen, total)})`, 115 | `Outdated (MS marketplace > Open VSX): ${outdated} (${calculatePercentage(outdated, total)})`, 116 | `Unstable (MS marketplace < Open VSX): ${unstable} (${calculatePercentage(unstable, total)})`, 117 | `Not in MS marketplace: ${notInMS} (${calculatePercentage(notInMS, total)})`, 118 | `Failed to publish: ${stat.failed.length} (${calculatePercentage(stat.failed.length, total)})`, 119 | "", 120 | "Microsoft:", 121 | `Total: ${msPublished} (${calculatePercentage(msPublished, total)})`, 122 | `Outdated: ${msPublishedOutdated.length}`, 123 | `Unstable: ${msPublishedUnstable.length}`, 124 | "", 125 | `Total resolutions: ${totalResolutions}`, 126 | `From release asset: ${fromReleaseAsset} (${calculatePercentage(fromReleaseAsset, totalResolutions)})`, 127 | `From release tag: ${fromReleaseTag} (${calculatePercentage(fromReleaseTag, totalResolutions)})`, 128 | `From repo tag: ${fromTag} (${calculatePercentage(fromTag, totalResolutions)})`, 129 | `From very latest repo commit of unmaintained (last update >= 2 months ago): ${fromLatestUnmaintained} (${calculatePercentage(fromLatestUnmaintained, totalResolutions)})`, 130 | `From very latest repo commit of not published to MS: ${fromLatestNotPublished} (${calculatePercentage(fromLatestNotPublished, totalResolutions)})`, 131 | `From very latest repo commit on the last update date: ${fromMatchedLatest} (${calculatePercentage(fromMatchedLatest, totalResolutions)})`, 132 | `From latest repo commit on the last update date: ${fromMatched} (${calculatePercentage(fromMatched, totalResolutions)})`, 133 | `Total resolved: ${totalResolved} (${calculatePercentage(totalResolved, totalResolutions)})`, 134 | "", 135 | `Updated in MS marketplace in month-to-date: ${updatedInMTD}`, 136 | `Of which updated in Open VSX within 2 days: ${updatedInOpenIn2Days.size} (${calculatePercentage(updatedInOpenIn2Days.size, updatedInMTD)})`, 137 | `Of which updated in Open VSX within 2 weeks: ${updatedInOpenIn2Weeks.size} (${calculatePercentage(updatedInOpenIn2Weeks.size, updatedInMTD)})`, 138 | `Of which updated in Open VSX within a month: ${updatedInOpenInMonth.size} (${calculatePercentage(updatedInOpenInMonth.size, updatedInMTD)})`, 139 | ); 140 | } else { 141 | if (total === 0) { 142 | summary.push("No extensions were processed"); 143 | } else { 144 | summary.push( 145 | `Up-to-date (MS Marketplace == Open VSX): ${upToDate} (${calculatePercentage(upToDate, total)})`, 146 | `Failed to publish: ${stat.failed.length} (${calculatePercentage(stat.failed.length, total)})`, 147 | `Outdated: ${msPublishedOutdated.length}`, 148 | `Unstable: ${msPublishedUnstable.length}`, 149 | ); 150 | } 151 | } 152 | 153 | console.log(summary.join(lineBreak)); 154 | summary.push("", "---", ""); 155 | 156 | let content: string[] = summary; 157 | 158 | if (outdated) { 159 | const keys = sortedKeys(stat.outdated); 160 | content.push("## Outdated (MS marketplace > Open VSX version)"); 161 | for (const id of keys) { 162 | const r = stat.outdated[id]; 163 | content.push( 164 | `${positionOf(id, keys)} ${generateMicrosoftLink(id)} (installs: ${humanNumber(r.msInstalls, formatter)}, daysInBetween: ${r.daysInBetween.toFixed(0)}): ${r.msVersion} > ${r.openVersion}`, 165 | ); 166 | } 167 | } 168 | 169 | if (notInOpen) { 170 | const keys = Object.keys(stat.notInOpen).sort( 171 | (a, b) => stat.notInOpen[b].msInstalls - stat.notInOpen[a].msInstalls, 172 | ); 173 | content.push("", "## Not published to Open VSX, but in MS marketplace"); 174 | for (const id of keys) { 175 | const r = stat.notInOpen[id]; 176 | content.push( 177 | `${positionOf(id, keys)} ${generateMicrosoftLink(id)} (installs: ${humanNumber(r.msInstalls, formatter)}): ${r.msVersion}`, 178 | ); 179 | } 180 | } 181 | 182 | if (unstable) { 183 | const keys = sortedKeys(stat.unstable); 184 | content.push("", "## Unstable (Open VSX > MS marketplace version)"); 185 | for (const id of keys) { 186 | const r = stat.unstable[id]; 187 | content.push( 188 | `${positionOf(id, keys)} ${generateMicrosoftLink(id)} (installs: ${humanNumber(r.msInstalls, formatter)}, daysInBetween: ${r.daysInBetween.toFixed(0)}): ${r.openVersion} > ${r.msVersion}`, 189 | ); 190 | } 191 | } 192 | 193 | if (notInMS) { 194 | content.push("", "## Not published to MS marketplace"); 195 | content.push(...stat.notInMS.map((ext) => `- ${generateOpenVsxLink(ext)}`)); 196 | } 197 | 198 | if (stat.failed.length) { 199 | content.push("", "## Failed to publish"); 200 | content.push(...stat.failed.map((ext) => `- ${generateMicrosoftLink(ext)}`)); 201 | } 202 | 203 | if ((unstable || stat.failed.length || outdated) && process.env.VALIDATE_PR === "true") { 204 | // Fail the validating job if there are failing extensions 205 | process.exitCode = 1; 206 | } 207 | 208 | /** 209 | * A threshold above which we will be alerted about a big extension breaking 210 | */ 211 | const threshold = 10_000_000; 212 | const existsFailingExtensionAboveThreshold = Object.keys(stat.outdated).some((id) => { 213 | const extension = stat.resolutions[id]; 214 | if (!extension?.msInstalls) { 215 | return false; 216 | } 217 | 218 | const aboveThreshold = extension.msInstalls > threshold; 219 | if (aboveThreshold) { 220 | console.log(`Extension ${id} is outdated and has ${humanNumber(extension.msInstalls)} installs`); 221 | } 222 | 223 | return aboveThreshold; 224 | }); 225 | 226 | // This should indicate a big extension breaking 227 | if (existsFailingExtensionAboveThreshold) { 228 | console.error( 229 | `There are outdated extensions above the threshold of ${humanNumber(threshold)} installs. See above for the list.`, 230 | ); 231 | process.exitCode = 1; 232 | } 233 | 234 | if (msPublished) { 235 | content.push("## MS extensions"); 236 | const publishedKeys = Object.keys(stat.msPublished).sort( 237 | (a, b) => stat.msPublished[b].msInstalls - stat.msPublished[a].msInstalls, 238 | ); 239 | const outdatedKeys = msPublishedOutdated.sort( 240 | (a, b) => stat.msPublished[b].msInstalls - stat.msPublished[a].msInstalls, 241 | ); 242 | const unstableKeys = msPublishedUnstable.sort( 243 | (a, b) => stat.msPublished[b].msInstalls - stat.msPublished[a].msInstalls, 244 | ); 245 | 246 | publishedKeys.forEach((id) => { 247 | const r = stat.msPublished[id]; 248 | content.push( 249 | `${positionOf(id, publishedKeys)} ${generateMicrosoftLink(id)} (installs: ${humanNumber(r.msInstalls, formatter)})`, 250 | ); 251 | }); 252 | 253 | content.push("## MS Outdated"); 254 | outdatedKeys.forEach((id) => { 255 | const r = stat.msPublished[id]; 256 | content.push( 257 | `${positionOf(id, outdatedKeys)} ${generateMicrosoftLink(id)} (installs: ${humanNumber(r.msInstalls, formatter)})`, 258 | ); 259 | }); 260 | 261 | content.push("## MS Unstable"); 262 | unstableKeys.forEach((id) => { 263 | const r = stat.msPublished[id]; 264 | content.push( 265 | `${positionOf(id, unstableKeys)} ${generateMicrosoftLink(id)} (installs: ${humanNumber(r.msInstalls, formatter)})`, 266 | ); 267 | }); 268 | } 269 | 270 | if (updatedInMTD) { 271 | content.push("## Updated in Open VSX within 2 days after in MS marketplace in MTD"); 272 | const keys = sortedKeys(stat.hitMiss); 273 | keys.forEach((id) => { 274 | const r = stat.hitMiss[id]; 275 | const in2Days = updatedInOpenIn2Days.has(id) ? "+" : "-"; 276 | const in2Weeks = updatedInOpenIn2Weeks.has(id) ? "+" : "-"; 277 | const inMonth = updatedInOpenInMonth.has(id) ? "+" : "-"; 278 | content.push( 279 | `${positionOf(id, keys)} ${inMonth}${in2Weeks}${in2Days} ${generateMicrosoftLink(id)}: installs: ${humanNumber(r.msInstalls, formatter)}; daysInBetween: ${r.daysInBetween?.toFixed(0)}; MS marketplace: ${r.msVersion}; Open VSX: ${r.openVersion}`, 280 | ); 281 | }); 282 | } 283 | 284 | if (upToDate) { 285 | content.push("## Up-to-date (Open VSX = MS marketplace version)"); 286 | const keys = Object.keys(stat.upToDate).sort((a, b) => stat.upToDate[b].msInstalls - stat.upToDate[a].msInstalls); 287 | keys.forEach((id) => { 288 | const r = stat.upToDate[id]; 289 | content.push( 290 | `${positionOf(id, keys)} ${generateMicrosoftLink(id)} (installs: ${humanNumber(r.msInstalls, formatter)}, daysInBetween: ${r.daysInBetween.toFixed(0)}): ${r.openVersion}`, 291 | ); 292 | }); 293 | } 294 | 295 | if (totalResolutions) { 296 | content.push("## Resolutions"); 297 | const keys = sortedKeys(stat.resolutions); 298 | keys.forEach((id) => { 299 | const extension = stat.resolutions[id]; 300 | const base = 301 | extension?.latest && !extension.msVersion 302 | ? `${positionOf(id, keys)} ${generateOpenVsxLink(id)} from '` 303 | : `${positionOf(id, keys)} ${generateMicrosoftLink(id)} (installs: ${humanNumber(extension.msInstalls!, formatter)}) from`; 304 | if (extension?.releaseAsset) { 305 | content.push(`${base} '${extension.releaseAsset}' release asset`); 306 | } else if (extension?.releaseTag) { 307 | content.push(`${base} '${extension.releaseTag}' release tag`); 308 | } else if (extension?.tag) { 309 | content.push(`${base} the '${extension.tag}' release tag`); 310 | } else if (extension?.latest) { 311 | if (extension.msVersion) { 312 | content.push( 313 | `${base} '${extension.latest}' - the very latest repo commit, since it is not actively maintained`, 314 | ); 315 | } else { 316 | content.push( 317 | `${base} '${extension.latest}' - the very latest repo commit, since it is not published to MS marketplace`, 318 | ); 319 | } 320 | } else if (extension?.matchedLatest) { 321 | content.push(`${base} '${extension.matchedLatest}' - the very latest commit on the last update date`); 322 | } else if (extension?.matched) { 323 | content.push(`${base} '${extension.matched}' - the latest commit on the last update date`); 324 | } else { 325 | content.push(`${base} unresolved`); 326 | } 327 | }); 328 | } 329 | 330 | await fs.promises.writeFile("/tmp/result.md", content.join(lineBreak), { encoding: "utf8" }); 331 | console.log("See result output for the detailed report."); 332 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=open-vsx_publish-extensions 2 | sonar.organization=open-vsx 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | #sonar.projectName=publish-extensions 6 | #sonar.projectVersion=1.0 7 | 8 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 9 | #sonar.sources=. 10 | 11 | # Encoding of the source code. Default is default system encoding 12 | #sonar.sourceEncoding=UTF-8 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["bun-types"], 4 | 5 | // enable latest features 6 | "lib": ["esnext", "node"], 7 | "module": "esnext", 8 | "target": "esnext", 9 | 10 | "moduleResolution": "bundler", 11 | "noEmit": true, 12 | "allowImportingTsExtensions": true, 13 | "emitDeclarationOnly": true, 14 | "composite": true, 15 | "moduleDetection": "force", 16 | 17 | "jsx": "react-jsx", 18 | "allowJs": true, 19 | "esModuleInterop": true, 20 | 21 | "strict": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "skipLibCheck": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2021 Gitpod and others 3 | * 4 | * This program and the accompanying materials are made available under the 5 | * terms of the Eclipse Public License v. 2.0 which is available at 6 | * http://www.eclipse.org/legal/epl-2.0. 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | ********************************************************************************/ 10 | 11 | //@ts-check 12 | 13 | export interface MSExtensionStat { 14 | msInstalls: number; 15 | msVersion: string; 16 | } 17 | 18 | export interface ExtensionStat extends MSExtensionStat { 19 | daysInBetween: number; 20 | openVersion: string; 21 | } 22 | 23 | export interface PublishStat { 24 | upToDate: { 25 | [id: string]: ExtensionStat; 26 | }; 27 | unstable: { 28 | [id: string]: ExtensionStat; 29 | }; 30 | outdated: { 31 | [id: string]: ExtensionStat; 32 | }; 33 | notInOpen: { 34 | [id: string]: MSExtensionStat; 35 | }; 36 | notInMS: string[]; 37 | 38 | resolutions: { 39 | [id: string]: Partial & ExtensionResolution; 40 | }; 41 | failed: string[]; 42 | 43 | msPublished: { 44 | [id: string]: MSExtensionStat; 45 | }; 46 | hitMiss: { 47 | [id: string]: ExtensionStat | ExtensionStat; 48 | }; 49 | } 50 | 51 | export interface Extensions { 52 | [id: string]: Omit; 53 | } 54 | 55 | export interface SingleExtensionQueryResult { 56 | publisher: Publisher; 57 | extensionId: string; 58 | extensionName: string; 59 | displayName: string; 60 | flags: number; 61 | lastUpdated: string; 62 | publishedDate: string; 63 | releaseDate: string; 64 | shortDescription: string; 65 | deploymentType: number; 66 | statistics: Statistic[]; 67 | } 68 | 69 | export interface Statistic { 70 | statisticName: string; 71 | value: number; 72 | } 73 | 74 | export interface Publisher { 75 | publisherId: string; 76 | publisherName: string; 77 | displayName: string; 78 | flags: number; 79 | domain: string; 80 | isDomainVerified: boolean; 81 | } 82 | 83 | export interface Extension { 84 | id: string; 85 | repository?: string; 86 | location?: string; 87 | prepublish?: string; 88 | extensionFile?: string; 89 | custom?: string[]; 90 | timeout?: number; 91 | target?: { 92 | [key: string]: 93 | | true 94 | | { 95 | env: { [key: string]: string }; 96 | }; 97 | }; 98 | msMarketplaceIdOverride?: string; 99 | pythonVersion?: string; 100 | } 101 | 102 | export interface ExtensionResolution { 103 | releaseAsset?: string; 104 | releaseTag?: string; 105 | tag?: string; 106 | latest?: string; 107 | matchedLatest?: string; 108 | matched?: string; 109 | } 110 | 111 | export interface ResolvedExtension { 112 | version: string; 113 | path: string; 114 | files?: { [key: string]: string }; 115 | resolution: ExtensionResolution; 116 | } 117 | 118 | export interface PublishContext { 119 | msVersion?: string; 120 | msLastUpdated?: Date; 121 | msInstalls?: number; 122 | msPublisher?: string; 123 | 124 | ovsxVersion?: string; 125 | ovsxLastUpdated?: Date; 126 | 127 | version?: string; 128 | files?: { [key: string]: string }; 129 | target: string; 130 | file?: string; 131 | repo?: string; 132 | ref?: string; 133 | 134 | environmentVariables?: { [key: string]: string }; 135 | } 136 | 137 | interface IRawGalleryExtensionProperty { 138 | readonly key: string; 139 | readonly value: string; 140 | } 141 | --------------------------------------------------------------------------------