diff --git a/.github/SETUP.md b/.github/SETUP.md new file mode 100644 index 0000000..24273b8 --- /dev/null +++ b/.github/SETUP.md @@ -0,0 +1,28 @@ +# GitHub Actions Setup + +## Required Secrets + +Add these in **Settings → Secrets and variables → Actions**: + +| Secret | Description | Where to get it | +|--------|-------------|-----------------| +| `AZURE_DEVOPS_PAT` | Personal Access Token | [marketplace.visualstudio.com/manage](https://marketplace.visualstudio.com/manage) → Create token with **Marketplace (Manage)** scope | +| `AZURE_DEVOPS_SHARE_WITH` | Orgs to share dev/hotfix builds | Comma-separated org names, e.g., `org1,org2` (optional) | + +Extension metadata (publisher, ID, name) is read from `vss-extension.json`. + +## Workflow Triggers + +- **`feature/*` branches** → Build + Publish to Development (e.g., `feature/1.0.0`) +- **`hotfix/*` branches** → Build + Publish to Hotfix (e.g., `hotfix/1.0.1`) +- **Tags** → Build + Publish to Release + Create GitHub Release (e.g., `1.0.0`) +- **Pull requests** → Build + Test only + +**Note:** Branch and tag names must contain a semver version number (e.g., `1.0.0`). + +## Optional: Environment Protection + +Add approval gates in **Settings → Environments**: +- Development +- Hotfix +- Release (recommended) diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 0000000..dc97856 --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,200 @@ +name: Build and Publish Extension + +on: + push: + branches: + - 'feature/**' + - 'hotfix/**' + tags: + - '*' + pull_request: + branches: + - main + +env: + NODE_VERSION: '20.x' + +jobs: + determine-environment: + runs-on: ubuntu-latest + outputs: + environment: ${{ steps.set-env.outputs.environment }} + should-publish: ${{ steps.set-env.outputs.should-publish }} + steps: + - name: Determine environment + id: set-env + run: | + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + echo "environment=Release" >> $GITHUB_OUTPUT + echo "should-publish=true" >> $GITHUB_OUTPUT + elif [[ "${{ github.ref }}" == refs/heads/feature/* ]]; then + echo "environment=Development" >> $GITHUB_OUTPUT + echo "should-publish=true" >> $GITHUB_OUTPUT + elif [[ "${{ github.ref }}" == refs/heads/hotfix/* ]]; then + echo "environment=Hotfix" >> $GITHUB_OUTPUT + echo "should-publish=true" >> $GITHUB_OUTPUT + else + echo "environment=Development" >> $GITHUB_OUTPUT + echo "should-publish=false" >> $GITHUB_OUTPUT + fi + + build: + runs-on: ubuntu-latest + needs: determine-environment + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies and build + run: | + npm run initdev + npm run build + + - name: Get version from tag or branch + id: version + run: | + COMMIT_COUNT=$(git rev-list --count HEAD) + + # For pull requests, use a test version + if [[ "${{ github.ref }}" == refs/pull/* ]]; then + VERSION="1.0.0-pr${{ github.event.pull_request.number }}" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Building PR version: $VERSION" + exit 0 + fi + + BRANCH_NAME="${GITHUB_REF#refs/heads/}" + BRANCH_NAME="${BRANCH_NAME#refs/tags/}" + + # Extract version number (e.g., from "feature/1.0.0" or "1.0.0") + if [[ "$BRANCH_NAME" =~ ([0-9]+\.[0-9]+\.[0-9]+) ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + else + echo "Error: Branch/tag name must contain a semver version (e.g., feature/1.0.0)" + exit 1 + fi + + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + VERSION="${VERSION_BASE}.${COMMIT_COUNT}" + elif [[ "${{ github.ref }}" == refs/heads/feature/* ]]; then + VERSION="${VERSION_BASE}.${COMMIT_COUNT}-feature" + elif [[ "${{ github.ref }}" == refs/heads/hotfix/* ]]; then + VERSION="${VERSION_BASE}.${COMMIT_COUNT}-hotfix" + else + VERSION="${VERSION_BASE}.${COMMIT_COUNT}" + fi + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Building version: $VERSION" + + - name: Package extension + run: | + npm run package:tasks -- --outputPath "${{ github.workspace }}/dist" --extensionVersion "${{ steps.version.outputs.version }}" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: vsix + path: dist/*.vsix + retention-days: 30 + + publish: + runs-on: ubuntu-latest + needs: [determine-environment, build] + if: needs.determine-environment.outputs.should-publish == 'true' + environment: ${{ needs.determine-environment.outputs.environment }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: vsix + path: dist + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install tfx-cli + run: npm install -g tfx-cli + + - name: Read extension metadata + id: metadata + run: | + PUBLISHER=$(jq -r '.publisher' vss-extension.json) + EXTENSION_ID=$(jq -r '.id' vss-extension.json) + echo "publisher=$PUBLISHER" >> $GITHUB_OUTPUT + echo "extension-id=$EXTENSION_ID" >> $GITHUB_OUTPUT + + - name: Set publish variables + id: publish-vars + env: + ENVIRONMENT: ${{ needs.determine-environment.outputs.environment }} + run: | + if [ "$ENVIRONMENT" = "Release" ]; then + echo "extension-tag=" >> $GITHUB_OUTPUT + echo "visibility=public" >> $GITHUB_OUTPUT + echo "share-with=" >> $GITHUB_OUTPUT + else + echo "extension-tag=-$ENVIRONMENT" >> $GITHUB_OUTPUT + echo "visibility=private" >> $GITHUB_OUTPUT + echo "share-with=${{ secrets.AZURE_DEVOPS_SHARE_WITH }}" >> $GITHUB_OUTPUT + fi + + - name: Unpublish old non-release extension + if: needs.determine-environment.outputs.environment != 'Release' + continue-on-error: true + env: + AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }} + run: | + tfx extension unpublish \ + --token "$AZURE_DEVOPS_PAT" \ + --publisher "${{ steps.metadata.outputs.publisher }}" \ + --extension-id "${{ steps.metadata.outputs.extension-id }}${{ steps.publish-vars.outputs.extension-tag }}" \ + --no-prompt + + - name: Publish to Azure DevOps Marketplace + env: + AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }} + run: | + SHARE_WITH="${{ steps.publish-vars.outputs.share-with }}" + VSIX_FILE=$(ls dist/*.vsix) + + if [ -n "$SHARE_WITH" ]; then + tfx extension publish \ + --token "$AZURE_DEVOPS_PAT" \ + --vsix "$VSIX_FILE" \ + --share-with $SHARE_WITH \ + --no-prompt + else + tfx extension publish \ + --token "$AZURE_DEVOPS_PAT" \ + --vsix "$VSIX_FILE" \ + --no-prompt + fi + + - name: Create GitHub Release + if: needs.determine-environment.outputs.environment == 'Release' + uses: softprops/action-gh-release@v1 + with: + files: dist/*.vsix + tag_name: ${{ github.ref_name }} + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 6b9c883..0000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,44 +0,0 @@ -trigger: - batch: true - branches: - include: - - refs/heads/feature/* - - refs/heads/hotfix/* - - refs/tags/* - -parameters: - - name: variable_group_name - displayName: Variable Group - type: string - default: 'DependencyCheck-AzureDevOps' - -pool: - vmImage: 'ubuntu-latest' - -variables: - - group: ${{ parameters.variable_group_name }} - - name: CurrentDate - value: $[ format('{0:yyyy}{0:MM}{0:dd}', pipeline.startTime) ] - - name: PublishEnvironment - ${{ if startsWith( variables['Build.SourceBranch'], 'refs/tags/' ) }}: - value: 'Release' - ${{ elseif startsWith( variables['Build.SourceBranch'], 'refs/heads/feature/' ) }}: - value: 'Development' - ${{ elseif startsWith( variables['Build.SourceBranch'], 'refs/heads/hotfix/' ) }}: - value: 'Hotfix' - ${{ else }}: - value: 'Development' - - -stages: - - stage: 'buildStage' - displayName: 'Build' - jobs: - - template: build/pipeline/build_job.yml - - stage: 'publishStage' - dependsOn: 'buildStage' - displayName: 'Publish' - jobs: - - template: build/pipeline/publish_job.yml - parameters: - PublishEnvironment: ${{ variables.PublishEnvironment }} diff --git a/build/pipeline/README.md b/build/pipeline/README.md deleted file mode 100644 index 0cbb6d7..0000000 --- a/build/pipeline/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# OWASP Dependency Check Azure DevOps Extension - -Build and deployment are automated through the use of an Azure DevOps Pipeline. - -## Azure DevOps Pipeline - -The pipeline needs a library called **"DependencyCheck-AzureDevOps"** with the following variables: - -| Name | Value | Description | -|---------------------------|------------------------|---------------------------------------------------------------------------------| -| extensionFileName | dependency-check.vsix | Extension file name | -| extensionId | dependencycheck | Extension Id | -| extensionName | OWASP Dependency Check | Extension name | -| gitHubConnectionName | *** | GitHub connection name on Azure DevOps "Service connections" | -| marketplaceConnectionName | *** | Visual Studio Marketplace connection name on Azure DevOps "Service connections" | -| publisher | dependency-check | Publisher used to publish on Visual Studio Marketplace | -| shareWith | *** | Comma separated list of Organization Ids to share non-production extension | - -The pipeline publishes a new release version every time a new tag is created on the repository. - -### Prerequisites - -For the correct execution of the pipeline must be installed in the organization the "[Azure DevOps Extension Tasks](https://marketplace.visualstudio.com/items?itemName=ms-devlabs.vsts-developer-tools-build-tasks)" extension available in the Visual Studio Marketplace \ No newline at end of file diff --git a/build/pipeline/build_job.yml b/build/pipeline/build_job.yml deleted file mode 100644 index 8df3485..0000000 --- a/build/pipeline/build_job.yml +++ /dev/null @@ -1,37 +0,0 @@ -jobs: - - job: 'buildJob' - displayName: 'Build' - steps: - - checkout: self - displayName: 'Checkout project repository' - clean: False - fetchDepth: 0 - fetchTags: True - - - task: PowerShell@2 - displayName: 'Set Build Number' - inputs: - filePath: '$(Build.SourcesDirectory)/build/pipeline/set-build-number.ps1' - workingDirectory: '$(Build.SourcesDirectory)' - env: - BUILD_REASON: $(Build.Reason) - BUILD_SOURCE_BRANCH: $(Build.SourceBranch) - BUILD_SOURCE_BRANCH_NAME: $(Build.SourceBranchName) - - - task: NodeTool@0 - inputs: - versionSpec: '18.x' - displayName: 'Install Node.js' - - - task: Npm@1 - displayName: 'Build extension' - inputs: - command: 'custom' - customCommand: run package -- -- --outputPath "$(Build.ArtifactStagingDirectory)/$(extensionFileName)" --publisher "$(publisher)" --extensionName $(extensionName) --extensionVersion "$(Build.SourceBranchName)" - - - task: PublishPipelineArtifact@1 - displayName: 'Publish artifact' - inputs: - publishLocation: 'pipeline' - targetPath: '$(Build.ArtifactStagingDirectory)' - artifact: 'vsix' \ No newline at end of file diff --git a/build/pipeline/publish_job.yml b/build/pipeline/publish_job.yml deleted file mode 100644 index 1f8c5e2..0000000 --- a/build/pipeline/publish_job.yml +++ /dev/null @@ -1,125 +0,0 @@ -parameters: - - name: PublishEnvironment - type: string -jobs: - - deployment: 'publishJob' - displayName: 'Publish ${{ parameters.PublishEnvironment }}' - environment: '${{ parameters.PublishEnvironment }}' - strategy: - runOnce: - deploy: - steps: - - checkout: self - displayName: 'Checkout project repository' - clean: False - fetchDepth: 0 - fetchTags: True - - - task: PowerShell@2 - displayName: 'Set Publish Variables' - inputs: - filePath: '$(Build.SourcesDirectory)/build/pipeline/set-publish-variables.ps1' - workingDirectory: '$(Build.SourcesDirectory)' - env: - BUILD_SOURCE_BRANCH: $(Build.SourceBranch) - BUILD_SOURCE_BRANCH_NAME: $(Build.SourceBranchName) - - - task: NodeTool@0 - inputs: - versionSpec: '18.x' - displayName: 'Install Node.js' - - - task: TfxInstaller@3 - displayName: 'Use Node CLI for Azure DevOps' - inputs: - version: '0.x' - checkLatest: true - - - ${{ if ne(parameters.PublishEnvironment, 'Release' ) }}: - - task: ExtractFiles@1 - displayName: 'Unpackage Extension' - inputs: - archiveFilePatterns: '$(Pipeline.Workspace)/vsix/$(extensionFileName)' - destinationFolder: '$(Agent.TempDirectory)/vsix-temp/' - cleanDestinationFolder: true - - - task: PowerShell@2 - displayName: 'Update Manifest' - inputs: - filePath: '$(Build.SourcesDirectory)/build/pipeline/update-manifest.ps1' - env: - BUILD_SOURCE_BRANCH: $(Build.SourceBranch) - BUILD_SOURCE_BRANCH_NAME: $(Build.SourceBranchName) - VSIX_TEMP_FOLDER: '$(Agent.TempDirectory)/vsix-temp/' - SOURCE_FOLDER: '$(Build.SourcesDirectory)' - - - task: ArchiveFiles@2 - displayName: 'Package Extension' - inputs: - rootFolderOrFile: '$(Agent.TempDirectory)/vsix-temp/' - includeRootFolder: false - archiveType: 'zip' - archiveFile: '$(Pipeline.Workspace)/vsix/$(extensionFileName)' - replaceExistingArchive: true - - - task: UnpublishAzureDevOpsExtension@4 - displayName: 'Unpublish old extension' - condition: and(succeeded(), ne('${{ parameters.PublishEnvironment }}', 'Release' )) - inputs: - connectTo: 'VsTeam' - connectedServiceName: '$(marketplaceConnectionName)' - method: 'id' - publisherId: '$(publisher)' - extensionId: '$(extensionId)' - extensionTag: '$(Publish.extensionTag)' - - - task: PublishAzureDevOpsExtension@4 - name: 'publishStep' - displayName: 'Publish to Marketplace' - inputs: - connectTo: 'VsTeam' - connectedServiceName: '$(marketplaceConnectionName)' - fileType: 'vsix' - vsixFile: '$(Pipeline.Workspace)/vsix/$(extensionFileName)' - publisherId: '$(publisher)' - extensionId: '$(extensionId)' - extensionName: '$(extensionName)$(Publish.extensionNameSuffix)' - extensionTag: '$(Publish.extensionTag)' - extensionVisibility: '$(Publish.extensionVisibility)' - updateTasksVersion: false - noWaitValidation: true - ${{ if ne(parameters.PublishEnvironment, 'Release' ) }}: - shareWith: '$(shareWith)' - - - task: PowerShell@2 - displayName: 'Set extension file name' - inputs: - filePath: '$(Build.SourcesDirectory)/build/pipeline/set-extension-file-name.ps1' - workingDirectory: '$(Build.SourcesDirectory)' - env: - GENERATED_FILE_NAME: $(publishStep.Extension.OutputPath) - - - task: IsAzureDevOpsExtensionValid@4 - inputs: - connectTo: 'VsTeam' - connectedServiceName: '$(marketplaceConnectionName)' - method: 'vsix' - vsixFile: '$(ExtensionFileName)' - - - task: PublishPipelineArtifact@1 - displayName: 'Publish vsix' - inputs: - publishLocation: 'pipeline' - targetPath: '$(ExtensionFileName)' - artifact: 'marketplace' - condition: succeededOrFailed() - - - ${{ if eq( parameters.PublishEnvironment, 'Release' ) }}: - - task: GitHubRelease@1 - displayName: 'GitHub Release Asset' - inputs: - gitHubConnection: '$(gitHubConnectionName)' - repositoryName: '$(Build.Repository.Name)' - action: 'edit' - tag: '$(Build.SourceBranchName)' - assets: '$(ExtensionFileName)' \ No newline at end of file