From 61bea636b11293be65a6db09af769e7f37242452 Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Tue, 11 Mar 2025 16:28:19 -0700 Subject: [PATCH 01/10] Add extension section to extension manifests --- extensions/audit-api/manifest.json | 9 ++++++++- extensions/audit-reports/manifest.json | 7 +++++++ extensions/datadog-prometheus-metrics/manifest.json | 7 +++++++ extensions/integration-session-manager/manifest.json | 7 +++++++ extensions/oauth-integration-debug/manifest.json | 7 +++++++ extensions/oauth-integration-validator/manifest.json | 9 ++++++++- extensions/publisher-command-center/manifest.json | 7 +++++++ extensions/reaper/manifest.json | 7 +++++++ 8 files changed, 58 insertions(+), 2 deletions(-) diff --git a/extensions/audit-api/manifest.json b/extensions/audit-api/manifest.json index 4a34fe37..6a6db35f 100644 --- a/extensions/audit-api/manifest.json +++ b/extensions/audit-api/manifest.json @@ -1434,5 +1434,12 @@ "checksum": "4c0e35070324acebc25693aa70024043" } }, - "users": null + "users": null, + "extension": { + "name": "audit-api", + "title": "Audit Log API", + "description": "An API Publisher can call for audit logs.", + "homepage": "https://github.com/posit-dev/connect-extensions/tree/main/extensions/audit-api", + "version": "0.0.0" + } } diff --git a/extensions/audit-reports/manifest.json b/extensions/audit-reports/manifest.json index 67e51d73..4aeaf545 100644 --- a/extensions/audit-reports/manifest.json +++ b/extensions/audit-reports/manifest.json @@ -39,5 +39,12 @@ "unpublished.qmd": { "checksum": "dd2d6de0ce972611a454e80776c4ed3d" } + }, + "extension": { + "name": "audit-reports", + "title": "Audit Reports", + "description": "A quarto site of audit reports.", + "homepage": "https://github.com/posit-dev/connect-extensions/tree/main/extensions/audit-reports", + "version": "0.0.0" } } diff --git a/extensions/datadog-prometheus-metrics/manifest.json b/extensions/datadog-prometheus-metrics/manifest.json index aa2da61c..41e91c03 100644 --- a/extensions/datadog-prometheus-metrics/manifest.json +++ b/extensions/datadog-prometheus-metrics/manifest.json @@ -26,5 +26,12 @@ "connect-extension.toml": { "checksum": "a47a27c677363b99083eae5af19482b4" } + }, + "extension": { + "name": "datadog-prometheus-metrics", + "title": "Plotting Connect Prometheus Metrics from Datadog", + "description": "Allows users to query Datadog for a selected Connect Prometheus metric over a given timeframe and view a plot of the results.", + "homepage": "https://github.com/posit-dev/connect-extensions/tree/main/extensions/datadog-prometheus-metrics", + "version": "0.0.0" } } diff --git a/extensions/integration-session-manager/manifest.json b/extensions/integration-session-manager/manifest.json index 201a0bd9..1d9053c7 100644 --- a/extensions/integration-session-manager/manifest.json +++ b/extensions/integration-session-manager/manifest.json @@ -29,5 +29,12 @@ "connect-extension.toml": { "checksum": "110f38ecd19febb0014b401396035605" } + }, + "extension": { + "name": "integration-session-manager", + "title": "Connect OAuth Integration Session Manager", + "description": "Allows Connect admins to view and manage OAuth integration sessions.", + "homepage": "https://github.com/posit-dev/connect-extensions/tree/main/extensions/integration-session-manager", + "version": "0.0.0" } } diff --git a/extensions/oauth-integration-debug/manifest.json b/extensions/oauth-integration-debug/manifest.json index fe12fb8d..1411f2ec 100644 --- a/extensions/oauth-integration-debug/manifest.json +++ b/extensions/oauth-integration-debug/manifest.json @@ -26,5 +26,12 @@ "connect-extension.toml": { "checksum": "1bed011ad19cbd33da1a44d9057c3298" } + }, + "extension": { + "name": "oauth-integration-debug", + "title": "Connect OAuth Integration Debugger", + "description": "Allows Connect content viewers to debug user session tokens and credential exchange.", + "homepage": "https://github.com/posit-dev/connect-extensions/tree/main/extensions/oauth-integration-debug", + "version": "0.0.0" } } diff --git a/extensions/oauth-integration-validator/manifest.json b/extensions/oauth-integration-validator/manifest.json index de5f0ac6..6be9b3d8 100644 --- a/extensions/oauth-integration-validator/manifest.json +++ b/extensions/oauth-integration-validator/manifest.json @@ -1391,5 +1391,12 @@ "checksum": "718c08fd6eaada9a0259cb6a9f9463be" } }, - "users": null + "users": null, + "extension": { + "name": "oauth-integration-validator", + "title": "Connect OAuth Integration Validator", + "description": "Test an OAuth integration by performing a GET request to a specified endpoint using the access token obtained by Connect.", + "homepage": "https://github.com/posit-dev/connect-extensions/tree/main/extensions/oauth-integration-validator", + "version": "0.0.0" + } } diff --git a/extensions/publisher-command-center/manifest.json b/extensions/publisher-command-center/manifest.json index 86ff8572..89953ccc 100644 --- a/extensions/publisher-command-center/manifest.json +++ b/extensions/publisher-command-center/manifest.json @@ -52,5 +52,12 @@ "requirements.txt": { "checksum": "a162a98758867a701ce693948ffdfa67" } + }, + "extension": { + "name": "publisher-command-center", + "title": "Publisher Command Center", + "description": "A dashboard for publishers to help manage and track their content", + "homepage": "https://github.com/posit-dev/connect-extensions/tree/main/extensions/publisher-command-center", + "version": "0.0.0" } } diff --git a/extensions/reaper/manifest.json b/extensions/reaper/manifest.json index 2dce98e9..d041ebb8 100644 --- a/extensions/reaper/manifest.json +++ b/extensions/reaper/manifest.json @@ -23,5 +23,12 @@ "connect-extension.toml": { "checksum": "c6e6af366dacce9c86f0a06901b931f3" } + }, + "extension": { + "name": "reaper", + "title": "Reaper", + "description": "A view to a kill.", + "homepage": "https://github.com/posit-dev/connect-extensions/tree/main/extensions/reaper", + "version": "0.0.0" } } From fa01edda2fff8ea8beeee60a496df92408c1ab97 Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Tue, 11 Mar 2025 16:28:45 -0700 Subject: [PATCH 02/10] Add empty start to extension list --- extensions.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 extensions.json diff --git a/extensions.json b/extensions.json new file mode 100644 index 00000000..98cd4a6c --- /dev/null +++ b/extensions.json @@ -0,0 +1,3 @@ +{ + "extensions": [] +} From ad870040ac9c5b9e87460e2eca25a099d6f82db6 Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Tue, 11 Mar 2025 16:29:04 -0700 Subject: [PATCH 03/10] Add extension GitHub actions --- .github/actions/lint-extension/action.yml | 38 ++++++++ .github/actions/package-extension/action.yml | 45 ++++++++++ .github/actions/release-extension/action.yml | 91 ++++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 .github/actions/lint-extension/action.yml create mode 100644 .github/actions/package-extension/action.yml create mode 100644 .github/actions/release-extension/action.yml diff --git a/.github/actions/lint-extension/action.yml b/.github/actions/lint-extension/action.yml new file mode 100644 index 00000000..f19b1c82 --- /dev/null +++ b/.github/actions/lint-extension/action.yml @@ -0,0 +1,38 @@ +name: Lint Extension +description: Lint an extension for release + +inputs: + extension-name: + description: The name of the extension + required: true + type: string + +runs: + using: "composite" + + steps: + # Ensures that the manifest.json for the given extension name + # contains all the required fields for the rest of the release workflow + - run: | + jq ' + if (.extension | type) != "object" then error("Missing extension object") + elif (.extension.name | type) != "string" then error("Missing extension.name") + elif (.extension.title | type) != "string" then error("Missing extension.title") + elif (.extension.description | type) != "string" then error("Missing extension.description") + elif (.extension.homepage | type) != "string" then error("Missing extension.homepage") + elif (.extension.version | type) != "string" then error("Missing extension.version") + else . end + ' ./extensions/${{ inputs.extension-name }}/manifest.json + shell: bash + + - uses: actions/setup-node@v4 + + - run: npm install -g semver + shell: bash + + # The semver must be valid for the sorting, comparisons, and release + # process to work + - name: Check for valid semver + run: | + semver -c $(jq -c -r '.extension.version' < ./extensions/${{ inputs.extension-name }}/manifest.json) + shell: bash diff --git a/.github/actions/package-extension/action.yml b/.github/actions/package-extension/action.yml new file mode 100644 index 00000000..ff0248bb --- /dev/null +++ b/.github/actions/package-extension/action.yml @@ -0,0 +1,45 @@ +name: Package Extension +description: Package an extension into a tarball for release + +inputs: + extension-name: + description: The name of the extension + required: true + type: string + artifact-name: + description: The name of the artifact + required: false + type: string + +runs: + using: "composite" + + steps: + # If we are not passed an artifact to use as the source for the tarball + # we will create a tarball from the extension's directory + - name: Create tar + if: ${{ inputs.artifact-name == '' }} + run: tar -czf ${{ inputs.extension-name}}.tar.gz ./extensions/${{ inputs.extension-name}} + shell: bash + + # If we are passed an artifact to use as the source for the tarball + # we will download the artifact and create a tarball from it + # this avoids including extra files in the extension + - name: Download optional artifact + if: ${{ inputs.artifact-name != '' }} + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.artifact-name }} + path: ${{ inputs.extension-name }} + + - name: Create tar from artifact + if: ${{ inputs.artifact-name != '' }} + run: tar -czf ${{ inputs.extension-name }}.tar.gz ${{ inputs.extension-name }} + shell: bash + + # Upload the extension's tarball for use in other actions in the workflow + - name: Upload extension tar + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.extension-name }}.tar.gz + path: ${{ inputs.extension-name }}.tar.gz diff --git a/.github/actions/release-extension/action.yml b/.github/actions/release-extension/action.yml new file mode 100644 index 00000000..c8b84d09 --- /dev/null +++ b/.github/actions/release-extension/action.yml @@ -0,0 +1,91 @@ +name: Release Extension +description: Release an extension as a GitHub Release + +inputs: + extension-name: + description: The name of the extension + required: true + type: string + +runs: + using: "composite" + + steps: + - uses: actions/setup-node@v4 + + - run: npm install -g semver + shell: bash + + - name: Get manifest extension version + run: | + MANIFEST_VERSION=$(semver -c $(jq -c -r '.extension.version' < ./extensions/${{ inputs.extension-name }}/manifest.json)) + echo "MANIFEST_VERSION=$MANIFEST_VERSION" >> "$GITHUB_ENV" + shell: bash + + # Grabs the latest version from the extensions.json file + # If an extension hasn't been released yet we default to `0.0.0` so any + # version in the manifest will be higher + - name: Get lastest version from extension list + continue-on-error: true + run: | + LATEST_VERSION=$(jq -c '.extensions[] | select(.name=="${{ inputs.extension-name }}").latestVersion.version' < extensions.json) + LATEST_VERSION=$(semver -c "${LATEST_VERSION:-0.0.0}") + echo "LATEST_VERSION=$LATEST_VERSION" >> "$GITHUB_ENV" + shell: bash + + # We only want to release if the manifest.json contains a newer semver + # version than the latest version in the extensions.json + # We compare that here, and echo if a release will occur + # This can be helpful when looking at Pull Request action outputs + # so it is clear what will happen on a merge to `main` + - name: Check if manifest has newer version + id: should_release + run: | + echo "The last released version is '$LATEST_VERSION' and the manifest version is '$MANIFEST_VERSION'" + HIGHER_VERSION=$(semver "$MANIFEST_VERSION" "$LATEST_VERSION" | tail -n 1) + if [ "$MANIFEST_VERSION" = "$HIGHER_VERSION" ] && [ "$MANIFEST_VERSION" != "$LATEST_VERSION" ]; then + echo "Should release: the manifest version is higher than the lastest version" + echo "should_release=true" >> "$GITHUB_OUTPUT" + else + echo "Should not release: the manifest version is not higher than the lastest version" + echo "should_release=false" >> "$GITHUB_OUTPUT" + fi + shell: bash + + # Here we download the packaged extension artifact to release + - uses: actions/download-artifact@v4 + if: github.ref_name == 'main' && steps.should_release.outputs.should_release == 'true' + with: + name: ${{ inputs.extension-name }}.tar.gz + + # The release tag utilizes both the extension name and semver version + # to create a unique tag for the repository + - name: Release tag + if: github.ref_name == 'main' && steps.should_release.outputs.should_release == 'true' + id: release_tag + run: | + RELEASE_TAG="${{ inputs.extension-name }}@v$MANIFEST_VERSION" + echo "RELEASE_TAG=$RELEASE_TAG" >> "$GITHUB_ENV" + shell: bash + + - name: Release + if: github.ref_name == 'main' && steps.should_release.outputs.should_release == 'true' + run: | + gh release create $RELEASE_TAG \ + --title "${{ inputs.extension-name }} v$MANIFEST_VERSION" \ + ${{ inputs.extension-name }}.tar.gz + shell: bash + + # We fetch the full release data to utilize in the extension list update + - name: Get release data + run: gh api /repos/${{ github.repository }}/releases/tags/$RELEASE_TAG > release-${{ inputs.extension-name }}.json + shell: bash + + # Each release data file is uploaded as an artifact for the extension list update + # The naming convention is `release-.json` to easily + # download each artifact matching the pattern + - name: Upload release data + uses: actions/upload-artifact@v4 + with: + name: release-${{ inputs.extension-name }}.json + path: release-${{ inputs.extension-name }}.json From 0cd1ad1c39bad564aa07ceaa40bc7f45394a1520 Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Tue, 11 Mar 2025 16:29:26 -0700 Subject: [PATCH 04/10] Add extension list script --- scripts/.gitignore | 13 +++ scripts/extension-list.ts | 162 ++++++++++++++++++++++++++++++++++++++ scripts/package-lock.json | 70 ++++++++++++++++ scripts/package.json | 16 ++++ scripts/tsconfig.json | 11 +++ scripts/types/index.ts | 24 ++++++ 6 files changed, 296 insertions(+) create mode 100644 scripts/.gitignore create mode 100644 scripts/extension-list.ts create mode 100644 scripts/package-lock.json create mode 100644 scripts/package.json create mode 100644 scripts/tsconfig.json create mode 100644 scripts/types/index.ts diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 00000000..23983acf --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1,13 @@ +!.env +!.env.* +coverage + +node_modules +dist +dist-ssr +out +npm-debug.log +out-html + +# Log files +npm-debug.log* diff --git a/scripts/extension-list.ts b/scripts/extension-list.ts new file mode 100644 index 00000000..9a848b4e --- /dev/null +++ b/scripts/extension-list.ts @@ -0,0 +1,162 @@ +import fs from "fs"; +import path from "path"; + +import semverValid from "semver/functions/valid"; +import semverEq from "semver/functions/eq"; +import semverRcompare from "semver/functions/rcompare"; + +import { Extension, ExtensionManifest, ExtensionVersion } from "./types"; + +function getExtensionNameFromRelease(release: any): string { + const tag = release.tag_name as string; + return tag.split("@")[0]; +} + +function getManifest(extensionName: string): ExtensionManifest { + // Runs from ./scripts/dist so go up an additional level + const manifestPath = path.join( + __dirname, + "..", + "..", + "extensions", + extensionName, + "manifest.json" + ); + return JSON.parse(fs.readFileSync(manifestPath, "utf8")); +} + +// Sort the given extension's version in descending order +function sortExtensionVersions(extension: Extension) { + extension.versions.sort((a, b) => semverRcompare(a.version, b.version)); +} + +class ExtensionList { + constructor(public extensions: Extension[]) { + this.extensions = extensions; + } + + static fromFile(path: string) { + const extensions = JSON.parse(fs.readFileSync(path, "utf8")).extensions; + return new ExtensionList(extensions); + } + + public addRelease(manifest: ExtensionManifest, githubRelease) { + const { name, title, description, homepage, version } = manifest.extension; + const { assets, published_at } = githubRelease; + + const { browser_download_url } = assets.find( + (asset) => asset.name === `${name}.tar.gz` + ); + + const newVersion = { + version, + released: published_at, + url: browser_download_url, + }; + + if (this.getExtension(name)) { + this.updateExtensionDetails(name, title, description, homepage); + this.addExtensionVersion(name, newVersion); + } else { + this.addNewExtension(name, title, description, homepage, newVersion); + } + } + + public getExtension(name: string): Extension | undefined { + return this.extensions.find((extension) => extension.name === name); + } + + private updateExtensionDetails( + name: string, + title: string, + description: string, + homepage: string + ) { + this.updateExtension(name, { + ...this.getExtension(name), + title, + description, + homepage, + }); + } + + private addExtensionVersion(name: string, version: ExtensionVersion) { + const extension = this.getExtension(name); + if (extension === undefined) { + throw new Error(`Extension ${name} does not exist in the list`); + } + // Check that the version is valid + if (!semverValid(version.version)) { + throw new Error(`Invalid version: ${version.version}`); + } + // Check if the version already exists + if (extension.versions.some((v) => semverEq(v.version, version.version))) { + throw new Error(`Version ${version.version} already exists`); + } + + // Add the version to the list + extension.versions.push(version); + sortExtensionVersions(extension); + + // Set the latest version to the newest semver released + extension.latestVersion = extension.versions[0]; + + this.updateExtension(extension.name, extension); + } + + private addNewExtension( + name: string, + title: string, + description: string, + homepage: string, + initialVersion: ExtensionVersion + ) { + if (this.getExtension(name) !== undefined) { + throw new Error(`Extension ${name} already exists in the list`); + } + this.extensions.push({ + name, + title, + description, + homepage, + latestVersion: initialVersion, + versions: [initialVersion], + }); + this.sortExtensions(); + } + + private updateExtension(name: string, data: Extension) { + const index = this.extensions.findIndex((ex) => ex.name === name); + if (index === -1) { + throw new Error(`Failed to update Extension ${name}, not found in list`); + } + this.extensions[index] = data; + } + + public stringify() { + return JSON.stringify({ extensions: this.extensions }, null, 2); + } + + private sortExtensions() { + this.extensions.sort((a, b) => a.name.localeCompare(b.name)); + } +} + +// Runs from ./scripts/dist so go up an additional level +const extensionListFilePath = path.join( + __dirname, + "..", + "..", + "extensions.json" +); + +const releases = JSON.parse(process.env.RELEASES); +const list = ExtensionList.fromFile(extensionListFilePath); + +releases.forEach((release) => { + const name = getExtensionNameFromRelease(release); + const manifest = getManifest(name); + list.addRelease(manifest, release); +}); + +fs.writeFileSync(extensionListFilePath, list.stringify()); diff --git a/scripts/package-lock.json b/scripts/package-lock.json new file mode 100644 index 00000000..6b990e85 --- /dev/null +++ b/scripts/package-lock.json @@ -0,0 +1,70 @@ +{ + "name": "connect-extensions-scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "connect-extensions-scripts", + "version": "1.0.0", + "dependencies": { + "semver": "^7.7.1" + }, + "devDependencies": { + "@types/node": "^22.13.9", + "@types/semver": "^7.5.8", + "typescript": "^5.8.2" + } + }, + "node_modules/@types/node": { + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 00000000..7cae0abe --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,16 @@ +{ + "name": "connect-extensions-scripts", + "version": "1.0.0", + "type": "commonjs", + "scripts": { + "update-extension-list": "tsc && node ./dist/extension-list.js" + }, + "dependencies": { + "semver": "^7.7.1" + }, + "devDependencies": { + "@types/node": "^22.13.9", + "@types/semver": "^7.5.8", + "typescript": "^5.8.2" + } +} diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 00000000..a18badcd --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2021", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "strict": false, + "skipLibCheck": true + } +} diff --git a/scripts/types/index.ts b/scripts/types/index.ts new file mode 100644 index 00000000..9134c243 --- /dev/null +++ b/scripts/types/index.ts @@ -0,0 +1,24 @@ +export interface ExtensionManifest { + extension: { + name: string; + title: string; + description: string; + homepage: string; + version: string; + }; +} + +export interface ExtensionVersion { + version: string; + released: string; + url: string; +} + +export interface Extension { + name: string; + title: string; + description: string; + homepage: string; + latestVersion: ExtensionVersion; + versions: ExtensionVersion[]; +} From 1e7675bc535b33d331ff68a2d0081e0519b8675a Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Tue, 11 Mar 2025 16:30:03 -0700 Subject: [PATCH 05/10] Replace extension workflows with a single workflow --- .github/workflows/audit-api.yaml | 26 --- .github/workflows/audit-reports.yaml | 26 --- .../workflows/datadog-prometheus-metrics.yaml | 25 --- .github/workflows/extensions.yml | 170 ++++++++++++++++++ .../workflows/integration-session-manager.yml | 25 --- .../workflows/oauth-integration-debug.yaml | 25 --- .../oauth-integration-validator.yaml | 25 --- .../workflows/publisher-command-center.yml | 36 ++-- .github/workflows/reaper.yml | 25 --- 9 files changed, 185 insertions(+), 198 deletions(-) delete mode 100644 .github/workflows/audit-api.yaml delete mode 100644 .github/workflows/audit-reports.yaml delete mode 100644 .github/workflows/datadog-prometheus-metrics.yaml create mode 100644 .github/workflows/extensions.yml delete mode 100644 .github/workflows/integration-session-manager.yml delete mode 100644 .github/workflows/oauth-integration-debug.yaml delete mode 100644 .github/workflows/oauth-integration-validator.yaml delete mode 100644 .github/workflows/reaper.yml diff --git a/.github/workflows/audit-api.yaml b/.github/workflows/audit-api.yaml deleted file mode 100644 index c9edd8d9..00000000 --- a/.github/workflows/audit-api.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: Audit API -on: - pull_request: - paths: - - 'extensions/audit-api/**' - -env: - EXTENSION_NAME: audit-api - -jobs: - package: - runs-on: ubuntu-latest - - steps: - - name: Check out repo - uses: actions/checkout@v4 - - - name: Create TAR - working-directory: ./extensions - run: tar -czf $EXTENSION_NAME.tar.gz $EXTENSION_NAME - - - name: Upload extension TAR - uses: actions/upload-artifact@v4 - with: - name: ${{ env.EXTENSION_NAME }}.tar.gz - path: extensions/${{ env.EXTENSION_NAME }}.tar.gz diff --git a/.github/workflows/audit-reports.yaml b/.github/workflows/audit-reports.yaml deleted file mode 100644 index 9421f723..00000000 --- a/.github/workflows/audit-reports.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: Audit Report -on: - pull_request: - paths: - - 'extensions/audit-reports/**' - -env: - EXTENSION_NAME: audit-reports - -jobs: - package: - runs-on: ubuntu-latest - - steps: - - name: Check out repo - uses: actions/checkout@v4 - - - name: Create TAR - working-directory: ./extensions - run: tar -czf $EXTENSION_NAME.tar.gz $EXTENSION_NAME - - - name: Upload extension TAR - uses: actions/upload-artifact@v4 - with: - name: ${{ env.EXTENSION_NAME }}.tar.gz - path: extensions/${{ env.EXTENSION_NAME }}.tar.gz diff --git a/.github/workflows/datadog-prometheus-metrics.yaml b/.github/workflows/datadog-prometheus-metrics.yaml deleted file mode 100644 index 33648684..00000000 --- a/.github/workflows/datadog-prometheus-metrics.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: Datadog Prometheus Metrics Extension - -on: - pull_request: - paths: - - 'extensions/datadog-prometheus-metrics/**' - -env: - EXTENSION_NAME: datadog-prometheus-metrics - -jobs: - package: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Create tar - working-directory: ./extensions - run: tar -czf $EXTENSION_NAME.tar.gz $EXTENSION_NAME - - - name: Upload extension tar - uses: actions/upload-artifact@v4 - with: - name: ${{ env.EXTENSION_NAME }}.tar.gz - path: extensions/${{ env.EXTENSION_NAME }}.tar.gz diff --git a/.github/workflows/extensions.yml b/.github/workflows/extensions.yml new file mode 100644 index 00000000..1b0eb0f3 --- /dev/null +++ b/.github/workflows/extensions.yml @@ -0,0 +1,170 @@ +name: Extension Workflow + +on: + pull_request: + push: + branches: + - main + +jobs: + # Detects file changes in extension directories that utilize the `simple-extensions` + # job below + simple-extension-changes: + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + # Expose matched filters as 'changes' output variable + changes: ${{ steps.changes.outputs.changes }} + + steps: + - uses: actions/checkout@v4 + + - uses: dorny/paths-filter@v3 + id: changes + with: + # Adding a new extension that has a directory that can be TARed? + # Add a new line here with the name of the extension and directory path + # Be sure the extension name and directory have the same name + # e.g. `extension-name: extensions/extension-name/**` + filters: | + reaper: extensions/reaper/** + integration-session-manager: extensions/integration-session-manager/** + + # Runs for each extension that has changed from `simple-extension-changes` + # Lints, packages, and releases the extension if the semver is updated. + simple-extensions: + needs: [simple-extension-changes] + # Will only run if there are changes in the simple extensions + # https://github.com/dorny/paths-filter/issues/66#issuecomment-778267385 + if: ${{ needs.simple-extension-changes.outputs.changes != '[]' && needs.simple-extension-changes.outputs.changes != '' }} + strategy: + # Do not fail fast so all extensions are processed + fail-fast: false + matrix: + # Parse JSON containing names of all filters matching any of changed extensions + # e.g. ['reaper'] if the reaper extension dir changed + extension: ${{ fromJSON(needs.simple-extension-changes.outputs.changes) }} + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/lint-extension + with: + extension-name: ${{ matrix.extension }} + + - uses: ./.github/actions/package-extension + with: + extension-name: ${{ matrix.extension }} + + # Extensions are only released when this workflow triggers on `main` + # otherwise, the release is skipped + # See the action comments for more details + - uses: ./.github/actions/release-extension + with: + extension-name: ${{ matrix.extension }} + + # Detects file changes for complex extension directories that require more + # than the `simple-extensions` job offers. + # For example, run a build script prior to packaging the extension TAR + complex-extension-changes: + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + # Adding a new extension with a complex build process? + # Add a new line here with the name of the extension step output variable below + # e.g. `extension-name: ${{ steps.changes.outputs.extension-name }}` + publisher-command-center: ${{ steps.changes.outputs.publisher-command-center }} + + steps: + - uses: actions/checkout@v4 + + - uses: dorny/paths-filter@v3 + id: changes + with: + # Adding a new extension that has a complex build process? + # Add a new line here with the name of the extension and directory path + # Be sure the extension name and directory have the same name + # e.g. `extension-name: extensions/extension-name/**` + filters: | + publisher-command-center: extensions/publisher-command-center/** + + # Creates and releases the Publisher Command Center extension using a custom + # workflow + publisher-command-center: + needs: [complex-extension-changes] + if: ${{ needs.complex-extension-changes.outputs.publisher-command-center == 'true' }} + uses: ./.github/workflows/publisher-command-center.yml + + # Gathers all release data from GitHub releases triggered by this workflow + # For use in the `update-extension-list` job + # If no releases were triggered the output for releases will be `[]` + fetch-releases: + runs-on: ubuntu-latest + needs: [simple-extensions, publisher-command-center] + if: ${{ always() }} + outputs: + releases: ${{ steps.fetch-releases.outputs.releases }} + + steps: + # Downloads every release data file from the release-extension action + # merging them under the .releases/ directory + - name: Download GitHub release data + uses: actions/download-artifact@v4 + with: + pattern: release-*.json + path: releases + merge-multiple: true + + # We use jq --slurp to create a single JSON array from all the JSON files + # to use in the `update-extension-list` job + - name: Fetch releases + id: fetch-releases + run: echo "releases=$(cat releases/*.json | jq -c --slurp .)" >> "$GITHUB_OUTPUT" + + # Updates the `extensions.json` file with the latest release data from + # all extensions that were released in this workflow using the `fetch-releases` + # job output + update-extension-list: + runs-on: ubuntu-latest + needs: [fetch-releases] + # Only runs if there are releases to update the extension list with + # https://github.com/actions/runner/issues/2205 + if: ${{ always() && needs.fetch-releases.result == 'success' && needs.fetch-releases.outputs.releases != '[]' }} + # Sets the RELEASES environment variable for the extension list update + # script to read in + env: + RELEASES: ${{ needs.fetch-releases.outputs.releases }} + + steps: + # Checkout main to commit the updated extension list + # reduces the chance of conflicts when updating the extension list with + # multiple running workflows + - uses: actions/checkout@v4 + with: + ref: main + + - uses: actions/setup-node@v4 + with: + node-version: "lts/*" + cache: "npm" + cache-dependency-path: scripts/package-lock.json + + - run: npm ci + working-directory: ./scripts + + - run: npm run update-extension-list + working-directory: ./scripts + + # Commits and pushes the updated extension list to the repository + # https://github.com/actions/checkout/tree/v4/?tab=readme-ov-file#push-a-commit-using-the-built-in-token + - run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add extensions.json + git commit -m "Update extension list" + git push diff --git a/.github/workflows/integration-session-manager.yml b/.github/workflows/integration-session-manager.yml deleted file mode 100644 index 3efdaf7f..00000000 --- a/.github/workflows/integration-session-manager.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Integration Session Manager Extension - -on: - pull_request: - paths: - - 'extensions/integration-session-manager/**' - -env: - EXTENSION_NAME: integration-session-manager - -jobs: - package: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Create tar - working-directory: ./extensions - run: tar -czf $EXTENSION_NAME.tar.gz $EXTENSION_NAME - - - name: Upload extension tar - uses: actions/upload-artifact@v4 - with: - name: ${{ env.EXTENSION_NAME }}.tar.gz - path: extensions/${{ env.EXTENSION_NAME }}.tar.gz diff --git a/.github/workflows/oauth-integration-debug.yaml b/.github/workflows/oauth-integration-debug.yaml deleted file mode 100644 index 63fe791d..00000000 --- a/.github/workflows/oauth-integration-debug.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: OAuth Integration Debug Extension - -on: - pull_request: - paths: - - 'extensions/oauth-integration-debug/**' - -env: - EXTENSION_NAME: oauth-integration-debug - -jobs: - package: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Create tar - working-directory: ./extensions - run: tar -czf $EXTENSION_NAME.tar.gz $EXTENSION_NAME - - - name: Upload extension tar - uses: actions/upload-artifact@v4 - with: - name: ${{ env.EXTENSION_NAME }}.tar.gz - path: extensions/${{ env.EXTENSION_NAME }}.tar.gz diff --git a/.github/workflows/oauth-integration-validator.yaml b/.github/workflows/oauth-integration-validator.yaml deleted file mode 100644 index 66e77268..00000000 --- a/.github/workflows/oauth-integration-validator.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: OAuth Integration Validator Extension - -on: - pull_request: - paths: - - 'extensions/oauth-integration-validator/**' - -env: - EXTENSION_NAME: oauth-integration-validator - -jobs: - package: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Create tar - working-directory: ./extensions - run: tar -czf $EXTENSION_NAME.tar.gz $EXTENSION_NAME - - - name: Upload extension tar - uses: actions/upload-artifact@v4 - with: - name: ${{ env.EXTENSION_NAME }}.tar.gz - path: extensions/${{ env.EXTENSION_NAME }}.tar.gz diff --git a/.github/workflows/publisher-command-center.yml b/.github/workflows/publisher-command-center.yml index 3f31a14b..c9929970 100644 --- a/.github/workflows/publisher-command-center.yml +++ b/.github/workflows/publisher-command-center.yml @@ -1,15 +1,14 @@ name: Publisher Commander Center Extension on: - pull_request: - paths: - - 'extensions/publisher-command-center/**' + workflow_call: env: EXTENSION_NAME: publisher-command-center + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: - build: + extension: runs-on: ubuntu-latest defaults: run: @@ -17,10 +16,15 @@ jobs: steps: - uses: actions/checkout@v4 + + - uses: ./.github/actions/lint-extension + with: + extension-name: ${{ env.EXTENSION_NAME }} + - uses: actions/setup-node@v4 with: - node-version: 'lts/*' - cache: 'npm' + node-version: "lts/*" + cache: "npm" cache-dependency-path: extensions/${{ env.EXTENSION_NAME }}/package-lock.json - run: npm ci @@ -37,21 +41,11 @@ jobs: extensions/${{ env.EXTENSION_NAME }}/manifest.json extensions/${{ env.EXTENSION_NAME }}/connect-extension.toml - package: - needs: build - runs-on: ubuntu-latest - - steps: - - uses: actions/download-artifact@v4 + - uses: ./.github/actions/package-extension with: - name: ${{ env.EXTENSION_NAME}} - path: ${{ env.EXTENSION_NAME }} + extension-name: ${{ env.EXTENSION_NAME }} + artifact-name: ${{ env.EXTENSION_NAME }} - - name: Create tar - run: tar -czf $EXTENSION_NAME.tar.gz $EXTENSION_NAME - - - name: Upload extension tar - uses: actions/upload-artifact@v4 + - uses: ./.github/actions/release-extension with: - name: ${{ env.EXTENSION_NAME }}.tar.gz - path: ${{ env.EXTENSION_NAME }}.tar.gz + extension-name: ${{ env.EXTENSION_NAME }} diff --git a/.github/workflows/reaper.yml b/.github/workflows/reaper.yml deleted file mode 100644 index 11ccb321..00000000 --- a/.github/workflows/reaper.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Reaper Extension - -on: - pull_request: - paths: - - 'extensions/reaper/**' - -env: - EXTENSION_NAME: reaper - -jobs: - package: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Create tar - working-directory: ./extensions - run: tar -czf $EXTENSION_NAME.tar.gz $EXTENSION_NAME - - - name: Upload extension tar - uses: actions/upload-artifact@v4 - with: - name: ${{ env.EXTENSION_NAME }}.tar.gz - path: extensions/${{ env.EXTENSION_NAME }}.tar.gz From f2969fcf63ff1bd35f4e0a85d6d3bf79e9c3d82b Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Tue, 11 Mar 2025 16:55:57 -0700 Subject: [PATCH 06/10] Include ref_name=main checks in steps in release --- .github/actions/release-extension/action.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/release-extension/action.yml b/.github/actions/release-extension/action.yml index c8b84d09..1b1d8091 100644 --- a/.github/actions/release-extension/action.yml +++ b/.github/actions/release-extension/action.yml @@ -78,6 +78,7 @@ runs: # We fetch the full release data to utilize in the extension list update - name: Get release data + if: github.ref_name == 'main' && steps.should_release.outputs.should_release == 'true' run: gh api /repos/${{ github.repository }}/releases/tags/$RELEASE_TAG > release-${{ inputs.extension-name }}.json shell: bash @@ -85,6 +86,7 @@ runs: # The naming convention is `release-.json` to easily # download each artifact matching the pattern - name: Upload release data + if: github.ref_name == 'main' && steps.should_release.outputs.should_release == 'true' uses: actions/upload-artifact@v4 with: name: release-${{ inputs.extension-name }}.json From c1bbb40af4a95dfdf8758feefb2540efe2878ca1 Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Fri, 14 Mar 2025 11:26:06 -0700 Subject: [PATCH 07/10] Add comment separating complex extensions section --- .github/workflows/extensions.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/extensions.yml b/.github/workflows/extensions.yml index 1b0eb0f3..e33fbe85 100644 --- a/.github/workflows/extensions.yml +++ b/.github/workflows/extensions.yml @@ -100,6 +100,10 @@ jobs: if: ${{ needs.complex-extension-changes.outputs.publisher-command-center == 'true' }} uses: ./.github/workflows/publisher-command-center.yml + + # All extensions have been linted, packaged, and released, if necessary + # Continuing to update the extension list with the latest release data + # Gathers all release data from GitHub releases triggered by this workflow # For use in the `update-extension-list` job # If no releases were triggered the output for releases will be `[]` From dab3a99372fe9fc12a26d96029cd9c7be0631a8f Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Fri, 14 Mar 2025 14:28:55 -0700 Subject: [PATCH 08/10] Improve release check messages --- .github/actions/release-extension/action.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/release-extension/action.yml b/.github/actions/release-extension/action.yml index 1b1d8091..e90beb42 100644 --- a/.github/actions/release-extension/action.yml +++ b/.github/actions/release-extension/action.yml @@ -41,13 +41,13 @@ runs: - name: Check if manifest has newer version id: should_release run: | - echo "The last released version is '$LATEST_VERSION' and the manifest version is '$MANIFEST_VERSION'" + echo "The manifest version is '$MANIFEST_VERSION' and the released version is '$LATEST_VERSION'" HIGHER_VERSION=$(semver "$MANIFEST_VERSION" "$LATEST_VERSION" | tail -n 1) if [ "$MANIFEST_VERSION" = "$HIGHER_VERSION" ] && [ "$MANIFEST_VERSION" != "$LATEST_VERSION" ]; then - echo "Should release: the manifest version is higher than the lastest version" + echo "🚀 Will release! The manifest version is greater than the released version." echo "should_release=true" >> "$GITHUB_OUTPUT" else - echo "Should not release: the manifest version is not higher than the lastest version" + echo "😴 Holding back from release: The manifest version is not greater than the released version." echo "should_release=false" >> "$GITHUB_OUTPUT" fi shell: bash From e5340888b0e86d4cb5616466e63b45d21d9181ae Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Mon, 17 Mar 2025 10:16:56 -0700 Subject: [PATCH 09/10] Clarify GitHub release data is fetched --- .github/actions/release-extension/action.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/release-extension/action.yml b/.github/actions/release-extension/action.yml index e90beb42..e324855c 100644 --- a/.github/actions/release-extension/action.yml +++ b/.github/actions/release-extension/action.yml @@ -76,7 +76,8 @@ runs: ${{ inputs.extension-name }}.tar.gz shell: bash - # We fetch the full release data to utilize in the extension list update + # We fetch the GitHub release using the GitHub API, storing the data + # for use in the extension list update. - name: Get release data if: github.ref_name == 'main' && steps.should_release.outputs.should_release == 'true' run: gh api /repos/${{ github.repository }}/releases/tags/$RELEASE_TAG > release-${{ inputs.extension-name }}.json From b3c640118158eb750911d39467507074e7e67c65 Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Tue, 18 Mar 2025 16:15:08 -0700 Subject: [PATCH 10/10] Include comments in publisher-cmd-center workflow --- .github/workflows/publisher-command-center.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/publisher-command-center.yml b/.github/workflows/publisher-command-center.yml index c9929970..870f3992 100644 --- a/.github/workflows/publisher-command-center.yml +++ b/.github/workflows/publisher-command-center.yml @@ -3,6 +3,8 @@ name: Publisher Commander Center Extension on: workflow_call: +# Setup the environment with the extension name for easy re-use +# Also need the GH_TOKEN for the release-extension action to be able to use gh env: EXTENSION_NAME: publisher-command-center GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -15,12 +17,16 @@ jobs: working-directory: ./extensions/${{ env.EXTENSION_NAME }} steps: + # Checkout the repository so the rest of the actions can run with no issue - uses: actions/checkout@v4 + # We want to fail quickly if the linting fails, do that first - uses: ./.github/actions/lint-extension with: extension-name: ${{ env.EXTENSION_NAME }} + # Publisher Command Center needs to setup node, install dependencies, and + # build to make the files needed for the extension - uses: actions/setup-node@v4 with: node-version: "lts/*" @@ -30,6 +36,11 @@ jobs: - run: npm ci - run: npm run build + # Now that the extension is built we need to upload an artifact to pass + # to the package-extension action that contains the files we want to be + # included in the extension + # This only includes necessary files for the extension to run leaving out + # the files that were used to build the /dist/ directory - name: Upload built extension uses: actions/upload-artifact@v4 with: @@ -41,11 +52,16 @@ jobs: extensions/${{ env.EXTENSION_NAME }}/manifest.json extensions/${{ env.EXTENSION_NAME }}/connect-extension.toml + # Package up the extension into a TAR using the generalized + # package-extension action - uses: ./.github/actions/package-extension with: extension-name: ${{ env.EXTENSION_NAME }} artifact-name: ${{ env.EXTENSION_NAME }} + # Release the extension using the release-extension action + # Will only create a GitHub release if merged to `main` and the semver + # version has been updated - uses: ./.github/actions/release-extension with: extension-name: ${{ env.EXTENSION_NAME }}