Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/actions/connect-integration-test/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: 'Run Connect Integration Test'
description: 'Run integration test for a single extension'

inputs:
extension-name:
description: 'Name of extension to test'
required: true
connect-version:
description: 'Connect version to test against'
required: true
connect-license:
description: 'Posit Connect license'
required: true

outputs:
test_status:
description: 'Status of the integration test'
value: ${{ steps.run-test.outputs.status }}
reports_path:
description: 'Path to test reports'
value: ${{ steps.run-test.outputs.reports_path }}

runs:
using: "composite"
steps:
- uses: actions/checkout@v4

# Setup Docker BuildX which is required for the integration tests
- uses: docker/setup-buildx-action@v3

- name: Write Connect license
shell: bash
run: echo "${{ inputs.connect-license }}" > ./integration/license.lic

# Here we download the packaged extension artifact created from the calling workflow
- uses: actions/download-artifact@v4
with:
name: ${{ inputs.extension-name }}.tar.gz
path: integration/bundles

- uses: astral-sh/setup-uv@v5
with:
pyproject-file: "./integration/pyproject.toml"

- name: Install isolated Python with UV
shell: bash
working-directory: ./integration
run: uv python install

# Run the test and capture the report path and status
- shell: bash
run: |
make -C ./integration ${{ inputs.connect-version }} \
EXTENSION_NAME=${{ inputs.extension-name }}
echo "reports_path=$(pwd)/integration/reports" >> $GITHUB_OUTPUT
echo "status=$?" >> $GITHUB_OUTPUT
130 changes: 130 additions & 0 deletions .github/actions/lint-extension/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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:
- name: Check extension name matches directory
run: |
MANIFEST_NAME=$(jq -r '.extension.name' < ./extensions/${{ inputs.extension-name }}/manifest.json)
if [ ${{ inputs.extension-name }} != $MANIFEST_NAME ]; then
echo "Error: Extension name, directory, and manifest.json mismatch"
echo "Extension '${{ inputs.extension-name }}' must be in the folder '/extensions/${{ inputs.extension-name }}'" and have the name '${{ inputs.extension-name }}' in the manifest.json.
exit 1
fi
shell: bash

# 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.minimumConnectVersion | type) != "string" then error("Missing extension.minimumConnectVersion")
elif (.extension.version | type) != "string" then error("Missing extension.version")
else . end
' ./extensions/${{ inputs.extension-name }}/manifest.json
shell: bash

# Ensures that the manifest.json has only valid requiredFeatures
- name: Check required features
run: |
# Read in valid requiredFeatures from extensions.json
VALID_FEATURES=$(jq -c '.requiredFeatures' ./extensions.json)

# Read in requiredFeatures from the extension's manifest.json
EXTENSION_REQUIRED_FEATURES=$(jq -c '.extension.requiredFeatures // []' ./extensions/${{ inputs.extension-name }}/manifest.json)

# Check if the extension's requiredFeatures is an array
if [ "$(jq -r 'type' <<< "$EXTENSION_REQUIRED_FEATURES")" != "array" ]; then
echo "Error: The requiredFeatures in manifest.json must be an array."
exit 1
fi

# Check if any extension requiredFeatures are not in the valid requiredFeatures list
INVALID_FEATURES=$(jq -n -c --argjson global "$VALID_FEATURES" --argjson extension "$EXTENSION_REQUIRED_FEATURES" '
$extension | map(. as $feature | if ($global | index($feature) | not) then $feature else empty end)
')

# Check if there are any invalid requiredFeatures
if [ "$INVALID_FEATURES" != "[]" ]; then
echo "Error: The following `requiredFeatures` in manifest.json are not defined in extensions.json:"
echo "$INVALID_FEATURES"
echo "Please add these features to the 'requiredFeatures' array in extensions.json or remove them from the manifest."
exit 1
fi
shell: bash

# Ensures that the manifest.json category is valid
- name: Check category
run: |
# Read in valid category IDs from extensions.json
VALID_CATEGORY_IDS=$(jq -c '[.categories[] | .id]' ./extensions.json)

# Read in category from the extension's manifest.json
EXTENSION_CATEGORY=$(jq -r '.extension.category // ""' ./extensions/${{ inputs.extension-name }}/manifest.json)

# If the extension's category is empty, skip validation
if [ -z "$EXTENSION_CATEGORY" ]; then
echo "No category specified in manifest.json"
exit 0
fi

# Check if extension category is in the valid categories ID list
CATEGORY_VALID=$(jq -n --argjson global "$VALID_CATEGORY_IDS" --arg category "$EXTENSION_CATEGORY" '
$global | index($category) != null
')

# Check if the category is valid
if [ "$CATEGORY_VALID" != "true" ]; then
echo "Error: The category '$EXTENSION_CATEGORY' in manifest.json is not defined in extensions.json"
echo "Valid categories are: $(jq -r 'join(", ")' <<< "$VALID_CATEGORY_IDS")"
echo "Please use one of the valid category IDs or add a new category to the 'categories' array in extensions.json."
exit 1
fi
shell: bash

# Ensures that the manifest.json has only valid tags
- name: Check tags
run: |
# Read in valid tags from extensions.json
VALID_TAGS=$(jq -c '.tags' ./extensions.json)

# Read in tags from the extension's manifest.json
EXTENSION_TAGS=$(jq -c '.extension.tags // []' ./extensions/${{ inputs.extension-name }}/manifest.json)

# Check if any extension tag is not in the valid tags list
INVALID_TAGS=$(jq -n -c --argjson global "$VALID_TAGS" --argjson extension "$EXTENSION_TAGS" '
$extension | map(. as $tag | if ($global | index($tag) | not) then $tag else empty end)
')

# Check if there are any invalid tags
if [ "$INVALID_TAGS" != "[]" ]; then
echo "Error: The following tags in manifest.json are not defined in extensions.json:"
echo "$INVALID_TAGS"
echo "Please add these tags to the 'tags' array in extensions.json or remove them from the manifest."
exit 1
fi
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
45 changes: 45 additions & 0 deletions .github/actions/package-extension/action.yml
Original file line number Diff line number Diff line change
@@ -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
115 changes: 115 additions & 0 deletions .github/actions/release-extension/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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: |
# Add the extension name to the summary header for clarity
echo "# Extension: ${{ inputs.extension-name }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

if [ "$MANIFEST_VERSION" = "0.0.0" ]; then
MESSAGE="⚠️ Version 0.0.0 is reserved and will never be released."
echo "$MESSAGE"
echo "$MESSAGE" >> $GITHUB_STEP_SUMMARY
echo "should_release=false" >> "$GITHUB_OUTPUT"
else
# Normal version comparison logic
VERSION_INFO="The manifest version is '$MANIFEST_VERSION' and the released version is '$LATEST_VERSION'"
echo "$VERSION_INFO"
echo "$VERSION_INFO" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

HIGHER_VERSION=$(semver "$MANIFEST_VERSION" "$LATEST_VERSION" | tail -n 1)
if [ "$MANIFEST_VERSION" = "$HIGHER_VERSION" ] && [ "$MANIFEST_VERSION" != "$LATEST_VERSION" ]; then
MESSAGE="🚀 Will release! The manifest version is greater than the released version."
echo "$MESSAGE"
echo "$MESSAGE" >> $GITHUB_STEP_SUMMARY
echo "should_release=true" >> "$GITHUB_OUTPUT"
else
MESSAGE="😴 Holding back from release: The manifest version is not greater than the released version."
echo "$MESSAGE"
echo "$MESSAGE" >> $GITHUB_STEP_SUMMARY
echo "should_release=false" >> "$GITHUB_OUTPUT"
fi
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 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
shell: bash

# Each release data file is uploaded as an artifact for the extension list update
# The naming convention is `release-<extension-name>.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
path: release-${{ inputs.extension-name }}.json
retention-days: 1
Loading