From c671cd7fbc4a15ce6d920b615a402d1862b2d2e3 Mon Sep 17 00:00:00 2001 From: Artem Sokolovskii Date: Mon, 29 Jul 2024 15:26:14 +0200 Subject: [PATCH] Wizard: Improve GitHub Workflow for loading plugin to the server The goal is to create a workflow that allows building the plugin on GitHub and uploading it to the server, making the plugin available in the QtCreator Extension Manager. Added registerPlugin.js script: - This script creates a plugin.json file compatible with the server API. - The file can be sent to the server, requiring TOKEN_QT in GitHub secrets and API_URL set in the script. Updated main build_cmake script: - Added a state that generates plugin.json and includes it in artifacts and releases. - Gives possibility use GitHub releases directly in QtCreator to load the plugin, even if the plugin data wasn't uploaded to the server. Added ./build directory to .gitignore. To use workflow: - Create in github repository->settings->Secrets and variables->Actions ->Repository secrets new TOKEN and set the value from your Qt Account - Add to the Repository secrets API_URL from your Qt Account - In github reposytory->Settings->Actions->General->Workflow permissions ->Read and write permissions set to true - Create relase tag in format "vx.x.x" e.g. "v0.0.1" by git tag v0.0.1 - Push chages and tag git push --tag origin main Change-Id: I2135e0684bd3560736ecf7be1d25199713661c39 Reviewed-by: Marcus Tillmanns --- .../templates/wizards/projects/git.ignore | 1 + .../github_scripts_plugin.json | 53 +++++++ .../github_scripts_registerPlugin.js | 147 ++++++++++++++++++ .../github_workflows_build_cmake.yml | 145 +++++++++-------- .../wizards/qtcreatorplugin/wizard.json | 8 + 5 files changed, 281 insertions(+), 73 deletions(-) create mode 100644 share/qtcreator/templates/wizards/qtcreatorplugin/github_scripts_plugin.json create mode 100644 share/qtcreator/templates/wizards/qtcreatorplugin/github_scripts_registerPlugin.js diff --git a/share/qtcreator/templates/wizards/projects/git.ignore b/share/qtcreator/templates/wizards/projects/git.ignore index 4a0b530afd2..fd4fe2fdf6b 100644 --- a/share/qtcreator/templates/wizards/projects/git.ignore +++ b/share/qtcreator/templates/wizards/projects/git.ignore @@ -34,6 +34,7 @@ Thumbs.db *.rc /.qmake.cache /.qmake.stash +/build # qtcreator generated files *.pro.user* diff --git a/share/qtcreator/templates/wizards/qtcreatorplugin/github_scripts_plugin.json b/share/qtcreator/templates/wizards/qtcreatorplugin/github_scripts_plugin.json new file mode 100644 index 00000000000..8b031dedbfb --- /dev/null +++ b/share/qtcreator/templates/wizards/qtcreatorplugin/github_scripts_plugin.json @@ -0,0 +1,53 @@ +{ + "name": "%{PluginName}", + "vendor": "%{VendorName}", + "tags": [ + "Some tag", + "Other tag" + ], + "compatibility": "Qt 6.0", + "platforms": [ + "Windows", + "macOS", + "Linux" + ], + "license": "os", + "version": "0.0.1", + "status": "draft", + "is_pack": false, + "released_at": null, + "version_history": [ + { + "version": "0.0.1", + "is_latest": true, + "released_at": "2024-01-24T15:00:00Z" + } + ], + "icon": "https://url/to/icon-image", + "small_icon": "https://url/to/small-icon-image", + "description_paragraphs": [ + { + "header": "Description", + "text": [ + "Put a short description of your plugin here" + ] + } + ], + "description_links": [ + { + "url": "https://www.qt.io", + "link_text": "Online documentation" + } + ], + "description_images": [ + { + "url": "https://www.qt.io/hs-fs/hubfs/Outrun_QT_INT_0310.jpg?width=600&name=Outrun_QT_INT_0310.jpg", + "image_label": "Example 1." + } + ], + "copyright": "%{Copyright}", + "download_history": { + "download_count": 0 + }, + "plugin_sets": [] +} diff --git a/share/qtcreator/templates/wizards/qtcreatorplugin/github_scripts_registerPlugin.js b/share/qtcreator/templates/wizards/qtcreatorplugin/github_scripts_registerPlugin.js new file mode 100644 index 00000000000..535fa86ef02 --- /dev/null +++ b/share/qtcreator/templates/wizards/qtcreatorplugin/github_scripts_registerPlugin.js @@ -0,0 +1,147 @@ +const fs = require('fs'); +const path = require('path'); + +const updatePluginData = (plugin, env, pluginQtcData) => { + const dictionary_platform = { + 'Windows': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-Windows-x64.7z`, + 'Linux': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-Linux-x64.7z`, + 'macOS': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-macOS-universal.7z` + }; + + plugin.core_compat_version = env.QT_CREATOR_VERSION_INTERNAL; + plugin.core_version = env.QT_CREATOR_VERSION_INTERNAL; + plugin.status = "draft"; + + plugin.plugins.forEach(pluginsEntry => { + pluginsEntry.url = dictionary_platform[plugin.host_os]; + pluginsEntry.meta_data = pluginQtcData; + }); + return plugin; +}; + +const createNewPluginData = (env, platform, pluginQtcData) => { + const pluginJson = { + "status": "draft", + "core_compat_version": "", + "core_version": "", + "host_os": platform, + "host_os_version": "0", // TODO: pass the real data + "host_os_architecture": "x86_64", // TODO: pass the real data + "plugins": [ + { + "url": "", + "size": 5000, // TODO: check if it is needed, pass the real data + "meta_data": {}, + "dependencies": [] + } + ] + }; + + updatePluginData(pluginJson, env, pluginQtcData); + return pluginJson; +} + +const updateServerPluginJson = (endJsonData, pluginQtcData, env) => { + // Update the global data in mainData + endJsonData.name = pluginQtcData.Name; + endJsonData.vendor = pluginQtcData.Vendor; + endJsonData.version = pluginQtcData.Version; + endJsonData.copyright = pluginQtcData.Copyright; + endJsonData.status = "draft"; + + endJsonData.version_history[0].version = pluginQtcData.Version; + + endJsonData.description_paragraphs = [ + { + header: "Description", + text: [ + pluginQtcData.Description + ] + } + ]; + + let found = false; + // Update or Add the plugin data for the current Qt Creator version + for (const plugin of endJsonData.plugin_sets) { + if (plugin.core_compat_version === env.QT_CREATOR_VERSION_INTERNAL) { + updatePluginData(plugin, env, pluginQtcData); + found = true; + } + } + + if (!found) { + for (const platform of ['Windows', 'Linux', 'macOS']) { + endJsonData.plugin_sets.push(createNewPluginData(env, platform, pluginQtcData)); + } + } + + // Save the updated JSON file + const serverPluginJsonPath = path.join(__dirname, `${env.PLUGIN_NAME}.json`); + fs.writeFileSync(serverPluginJsonPath, JSON.stringify(endJsonData, null, 2), 'utf8'); +}; + +const request = async (type, url, token, data) => { + const response = await fetch(url, { + method: type, + headers: { + 'Authorization': `Bearer ${token}`, + 'accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: data ? JSON.stringify(data) : undefined + }); + if (!response.ok) { + const errorResponse = await response.json(); + console.error(`${type} Request Error Response:`, errorResponse); // Log the error response + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); +} + +const put = (url, token, data) => request('PUT', url, token, data) +const post = (url, token, data) => request('POST', url, token, data) +const get = (url, token) => request('GET', url, token) + +const purgeCache = async (env) => { + try { + await post(`${env.API_URL}api/v1/cache/purgeall`, env.TOKEN, {}); + console.log('Cache purged successfully'); + } catch (error) { + console.error('Error:', error); + } +}; + +async function main() { + const env = { + PLUGIN_DOWNLOAD_URL: process.env.PLUGIN_DOWNLOAD_URL || process.argv[2], + PLUGIN_NAME: process.env.PLUGIN_NAME || process.argv[3], + QT_CREATOR_VERSION: process.env.QT_CREATOR_VERSION || process.argv[4], + QT_CREATOR_VERSION_INTERNAL: process.env.QT_CREATOR_VERSION_INTERNAL || process.argv[5], + TOKEN: process.env.TOKEN || process.argv[6], + API_URL: process.env.API_URL || process.argv[7] || '' + }; + + const pluginQtcData = require(`../../${env.PLUGIN_NAME}-origin/${env.PLUGIN_NAME}.json`); + const templateFileData = require('./plugin.json'); + + if (env.API_URL === '') { + updateServerPluginJson(templateFileData, pluginQtcData, env); + process.exit(0); + } + + const response = await get(`${env.API_URL}api/v1/admin/extensions?search=${env.PLUGIN_NAME}`, env.TOKEN); + if (response.items.length > 0 && response.items[0].extension_id !== '') { + const pluginId = response.items[0].extension_id; + console.log('Plugin found. Updating the plugin'); + updateServerPluginJson(response.items[0], pluginQtcData, env); + + await put(`${env.API_URL}api/v1/admin/extensions/${pluginId}`, env.TOKEN, response.items[0]); + } else { + console.log('No plugin found. Creating a new plugin'); + updateServerPluginJson(templateFileData, pluginQtcData, env); + await post(`${env.API_URL}api/v1/admin/extensions`, env.TOKEN, templateFileData); + } + // await purgeCache(env); +} + +main().then(() => console.log('JSON file updated successfully')); diff --git a/share/qtcreator/templates/wizards/qtcreatorplugin/github_workflows_build_cmake.yml b/share/qtcreator/templates/wizards/qtcreatorplugin/github_workflows_build_cmake.yml index d5e53f033b5..09cb8f2d2f9 100644 --- a/share/qtcreator/templates/wizards/qtcreatorplugin/github_workflows_build_cmake.yml +++ b/share/qtcreator/templates/wizards/qtcreatorplugin/github_workflows_build_cmake.yml @@ -6,7 +6,7 @@ env: PLUGIN_NAME: %{PluginName} QT_VERSION: %{JS: Util.qtVersion()} QT_CREATOR_VERSION: %{JS: Util.qtCreatorVersion()} - QT_CREATOR_SNAPSHOT: NO + QT_CREATOR_VERSION_INTERNAL: %{JS: Util.qtCreatorIdeVersion()} MACOS_DEPLOYMENT_TARGET: "11.0" CMAKE_VERSION: "3.29.6" NINJA_VERSION: "1.12.1" @@ -23,74 +23,44 @@ jobs: - { name: "Windows Latest MSVC", artifact: "Windows-x64", os: windows-latest, + platform: windows_x64, cc: "cl", cxx: "cl", environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat", } - { name: "Ubuntu Latest GCC", artifact: "Linux-x64", os: ubuntu-latest, + platform: linux_x64, cc: "gcc", cxx: "g++" } - { name: "macOS Latest Clang", artifact: "macOS-universal", os: macos-latest, + platform: mac_x64, cc: "clang", cxx: "clang++" } steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Checkout submodules id: git shell: cmake -P {0} run: | if (${{github.ref}} MATCHES "tags/v(.*)") - file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}\\n") + file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}") else() - file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}\\n") + file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}") endif() - name: Download Ninja and CMake - shell: cmake -P {0} - run: | - set(cmake_version "$ENV{CMAKE_VERSION}") - set(ninja_version "$ENV{NINJA_VERSION}") - - if ("${{ runner.os }}" STREQUAL "Windows") - set(ninja_suffix "win.zip") - set(cmake_suffix "windows-x86_64.zip") - set(cmake_dir "cmake-${cmake_version}-windows-x86_64/bin") - elseif ("${{ runner.os }}" STREQUAL "Linux") - set(ninja_suffix "linux.zip") - set(cmake_suffix "linux-x86_64.tar.gz") - set(cmake_dir "cmake-${cmake_version}-linux-x86_64/bin") - elseif ("${{ runner.os }}" STREQUAL "macOS") - set(ninja_suffix "mac.zip") - set(cmake_suffix "macos-universal.tar.gz") - set(cmake_dir "cmake-${cmake_version}-macos-universal/CMake.app/Contents/bin") - endif() - - set(ninja_url "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-${ninja_suffix}") - file(DOWNLOAD "${ninja_url}" ./ninja.zip SHOW_PROGRESS) - execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./ninja.zip) - - set(cmake_url "https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-${cmake_suffix}") - file(DOWNLOAD "${cmake_url}" ./cmake.zip SHOW_PROGRESS) - execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./cmake.zip) - - # Add to PATH environment variable - file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/${cmake_dir}" cmake_dir) - set(path_separator ":") - if ("${{ runner.os }}" STREQUAL "Windows") - set(path_separator ";") - endif() - file(APPEND "$ENV{GITHUB_PATH}" "$ENV{GITHUB_WORKSPACE}${path_separator}${cmake_dir}") - - if (NOT "${{ runner.os }}" STREQUAL "Windows") - execute_process( - COMMAND chmod +x ninja - COMMAND chmod +x ${cmake_dir}/cmake - ) - endif() + uses: lukka/get-cmake@latest + with: + cmakeVersion: ${{ env.CMAKE_VERSION }} + ninjaVersion: ${{ env.NINJA_VERSION }} - name: Install system libs shell: cmake -P {0} @@ -184,40 +154,18 @@ jobs: endif() - name: Download Qt Creator + uses: qt-creator/install-dev-package@v1.2 + with: + version: ${{ env.QT_CREATOR_VERSION }} + unzip-to: 'qtcreator' + + - name: Extract Qt Creator id: qt_creator shell: cmake -P {0} run: | - string(REGEX MATCH "([0-9]+.[0-9]+).[0-9]+" outvar "$ENV{QT_CREATOR_VERSION}") - - set(qtc_base_url "https://download.qt.io/official_releases/qtcreator/${CMAKE_MATCH_1}/$ENV{QT_CREATOR_VERSION}/installer_source") - set(qtc_snapshot "$ENV{QT_CREATOR_SNAPSHOT}") - if (qtc_snapshot) - set(qtc_base_url "https://download.qt.io/snapshots/qtcreator/${CMAKE_MATCH_1}/$ENV{QT_CREATOR_VERSION}/installer_source/${qtc_snapshot}") - endif() - - if ("${{ runner.os }}" STREQUAL "Windows") - set(qtc_platform "windows_x64") - elseif ("${{ runner.os }}" STREQUAL "Linux") - set(qtc_platform "linux_x64") - elseif ("${{ runner.os }}" STREQUAL "macOS") - set(qtc_platform "mac_x64") - endif() - file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qtcreator" qtc_dir) - # Save the path for other steps file(APPEND "$ENV{GITHUB_OUTPUT}" "qtc_dir=${qtc_dir}") - file(MAKE_DIRECTORY qtcreator) - - message("Downloading Qt Creator from ${qtc_base_url}/${qtc_platform}") - - foreach(package qtcreator qtcreator_dev) - file(DOWNLOAD - "${qtc_base_url}/${qtc_platform}/${package}.7z" ./${package}.7z SHOW_PROGRESS) - execute_process(COMMAND - ${CMAKE_COMMAND} -E tar xvf ../${package}.7z WORKING_DIRECTORY qtcreator) - endforeach() - - name: Build shell: cmake -P {0} run: | @@ -276,11 +224,62 @@ jobs: path: ./${{ env.PLUGIN_NAME }}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z name: ${{ env.PLUGIN_NAME}}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z - release: + # The json is the same for all platforms, but we need to save one + - name: Upload plugin json + if: matrix.config.os == 'ubuntu-latest' + uses: actions/upload-artifact@v4 + with: + name: ${{ env.PLUGIN_NAME }}-origin-json + path: ./build/build/${{ env.PLUGIN_NAME }}.json + + update_json: if: contains(github.ref, 'tags/v') runs-on: ubuntu-latest needs: build + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Download the JSON file + uses: actions/download-artifact@v4 + with: + name: ${{ env.PLUGIN_NAME }}-origin-json + path: ./${{ env.PLUGIN_NAME }}-origin + + - name: Store Release upload_url + run: | + RELEASE_HTML_URL=$(echo "${{github.event.repository.html_url}}/releases/download/v${{ needs.build.outputs.tag }}") + echo "RELEASE_HTML_URL=${RELEASE_HTML_URL}" >> $GITHUB_ENV + + - name: Run the Node.js script to update JSON + env: + QT_TOKEN: ${{ secrets.TOKEN }} + API_URL: ${{ secrets.API_URL }} + run: | + node .github/scripts/registerPlugin.js \ + ${{ env.RELEASE_HTML_URL }} ${{ env.PLUGIN_NAME }} \ + ${{ env.QT_CREATOR_VERSION }} ${{ env.QT_CREATOR_VERSION_INTERNAL }} \ + ${{ env.QT_TOKEN }} ${{ env.API_URL }} + + - name: Delete previous json artifacts + uses: geekyeggo/delete-artifact@v5 + with: + name: ${{ env.PLUGIN_NAME }}*-json + + - name: Upload the modified JSON file as an artifact + uses: actions/upload-artifact@v4 + with: + name: plugin-json + path: .github/scripts/${{ env.PLUGIN_NAME }}.json + + release: + if: contains(github.ref, 'tags/v') + runs-on: ubuntu-latest + needs: [build, update_json] + steps: - name: Download artifacts uses: actions/download-artifact@v4 diff --git a/share/qtcreator/templates/wizards/qtcreatorplugin/wizard.json b/share/qtcreator/templates/wizards/qtcreatorplugin/wizard.json index bbbebb40fba..a1e9bb00aa1 100644 --- a/share/qtcreator/templates/wizards/qtcreatorplugin/wizard.json +++ b/share/qtcreator/templates/wizards/qtcreatorplugin/wizard.json @@ -164,6 +164,14 @@ "source": "github_workflows_README.md", "target": ".github/workflows/README.md" }, + { + "source": "github_scripts_registerPlugin.js", + "target": ".github/scripts/registerPlugin.js" + }, + { + "source": "github_scripts_plugin.json", + "target": ".github/scripts/plugin.json" + }, { "source": "myplugin.cpp", "target": "%{SrcFileName}"