Build Release Assets #12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build Release Assets | |
| on: | |
| release: | |
| types: [created] | |
| workflow_dispatch: | |
| inputs: | |
| release_tag: | |
| description: 'Release tag to attach build to (e.g., v1.0.0)' | |
| required: true | |
| override_assets: | |
| description: 'Override existing release assets' | |
| required: true | |
| default: 'true' | |
| type: choice | |
| options: | |
| - 'true' | |
| - 'false' | |
| jobs: | |
| build: | |
| name: Build Windows Executable and Archive | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| cache: 'pip' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt | |
| pip install pyinstaller | |
| - name: Determine version and tag | |
| id: get_version | |
| run: | | |
| if ("${{ github.event_name }}" -eq "release") { | |
| $TAG = "${{ github.event.release.tag_name }}" | |
| } else { | |
| $TAG = "${{ github.event.inputs.release_tag }}" | |
| } | |
| $VERSION = $TAG -replace "^v", "" | |
| echo "TAG=$TAG" | Out-File -FilePath $env:GITHUB_OUTPUT -Append | |
| echo "VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_OUTPUT -Append | |
| shell: pwsh | |
| - name: Update version file (src/version.py) | |
| run: | | |
| echo "__version__ = '${{ steps.get_version.outputs.VERSION }}'" | Out-File -FilePath src/version.py -Encoding utf8 | |
| shell: pwsh | |
| - name: Build application (one-dir) | |
| run: pyinstaller build.spec | |
| - name: Prepare files for archive | |
| id: prep_archive | |
| run: | | |
| $VERSION = "${{ steps.get_version.outputs.VERSION }}" | |
| # This is the directory name created by PyInstaller inside 'dist' | |
| # e.g., if your .spec or command names the output 'VCM-1.0.0', this is it. | |
| $PYINSTALLER_OUTPUT_DIR_NAME = "VCM-$VERSION" | |
| # This is the full path to the PyInstaller output directory containing your app files | |
| $PYINSTALLER_APP_CONTENT_SOURCE_PATH = "dist/$PYINSTALLER_OUTPUT_DIR_NAME" | |
| $ARCHIVE_STAGING_DIR = "VCM_Release_Package" # Temporary directory for assembling ZIP contents | |
| New-Item -ItemType Directory -Force -Path $ARCHIVE_STAGING_DIR | |
| # Copy CONTENTS of PyInstaller output folder (e.g., YourApp.exe, _internal, dlls) | |
| # directly into the root of the staging directory. | |
| Write-Host "Copying contents of PyInstaller output from '$PYINSTALLER_APP_CONTENT_SOURCE_PATH/*' to '$ARCHIVE_STAGING_DIR/'" | |
| if (Test-Path "$PYINSTALLER_APP_CONTENT_SOURCE_PATH") { | |
| if ((Get-ChildItem -Path $PYINSTALLER_APP_CONTENT_SOURCE_PATH).Count -gt 0) { | |
| Copy-Item -Path "$PYINSTALLER_APP_CONTENT_SOURCE_PATH/*" -Destination "$ARCHIVE_STAGING_DIR/" -Recurse -Force | |
| } else { | |
| Write-Warning "PyInstaller output directory '$PYINSTALLER_APP_CONTENT_SOURCE_PATH' is empty." | |
| # Decide if this is an error or can be skipped depending on your build | |
| } | |
| } else { | |
| Write-Error "PyInstaller output directory '$PYINSTALLER_APP_CONTENT_SOURCE_PATH' not found." | |
| exit 1 | |
| } | |
| # Copy example config file to the root of the staging directory | |
| Write-Host "Copying 'src/config.yml' to '$ARCHIVE_STAGING_DIR/config.yml'" | |
| Copy-Item -Path "src/config.yml" -Destination "$ARCHIVE_STAGING_DIR/config.yml" -Force | |
| $PACKAGING_ARTIFACTS_SOURCE_DIR = "packaging-artifacts" # Source folder in repo | |
| if (Test-Path $PACKAGING_ARTIFACTS_SOURCE_DIR) { | |
| Write-Host "Copying contents of '$PACKAGING_ARTIFACTS_SOURCE_DIR/*' directly into '$ARCHIVE_STAGING_DIR/'" | |
| Copy-Item -Path "$PACKAGING_ARTIFACTS_SOURCE_DIR/*" -Destination "$ARCHIVE_STAGING_DIR/" -Recurse -Force | |
| } else { | |
| Write-Host "'$PACKAGING_ARTIFACTS_SOURCE_DIR' not found, skipping." | |
| } | |
| echo "ARCHIVE_CONTENT_PATH=$ARCHIVE_STAGING_DIR" | Out-File -FilePath $env:GITHUB_OUTPUT -Append | |
| shell: pwsh | |
| - name: Create ZIP archive | |
| run: | | |
| $VERSION = "${{ steps.get_version.outputs.VERSION }}" | |
| $ARCHIVE_CONTENT_PATH = "${{ steps.prep_archive.outputs.ARCHIVE_CONTENT_PATH }}" | |
| $ZIP_FILE_NAME = "VCM/dist/VCM-${VERSION}-x64.zip" # Output zip to dist/ | |
| # Powershell's Compress-Archive creates a top-level folder in the zip from the source. | |
| # To get the desired structure (contents of $ARCHIVE_CONTENT_PATH directly at zip root): | |
| Push-Location $ARCHIVE_CONTENT_PATH | |
| Compress-Archive -Path "./*" -DestinationPath "../../$ZIP_FILE_NAME" -Force # Go up two levels to place zip in dist/ | |
| Pop-Location | |
| Write-Host "Created $ZIP_FILE_NAME" | |
| shell: pwsh | |
| - name: Check if release exists | |
| id: check_release | |
| if: github.event_name == 'workflow_dispatch' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| try { | |
| const tag = '${{ steps.get_version.outputs.TAG }}'; | |
| const release = await github.rest.repos.getReleaseByTag({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| tag: tag | |
| }); | |
| core.setOutput('exists', 'true'); | |
| return release.data.id; | |
| } catch (error) { | |
| core.setOutput('exists', 'false'); | |
| core.setFailed(`No release found with tag ${tag}: ${error.message}`); | |
| return null; | |
| } | |
| result-encoding: string | |
| - name: Delete existing assets if override requested | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.override_assets == 'true' && steps.check_release.outputs.exists == 'true' | |
| uses: actions/github-script@v6 | |
| with: | |
| script: | | |
| const releaseId = '${{ steps.check_release.outputs.result }}'; | |
| if (!releaseId) return; | |
| const version = '${{ steps.get_version.outputs.VERSION }}'; | |
| const assetPatterns = [ | |
| `VCM-${version}-x64.zip`, | |
| `VCM-${version}-x64.exe` | |
| ]; | |
| const assets = await github.rest.repos.listReleaseAssets({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| release_id: releaseId | |
| }); | |
| for (const asset of assets.data) { | |
| for (const pattern of assetPatterns) { | |
| if (asset.name.includes(pattern)) { | |
| console.log(`Deleting existing asset: ${asset.name}`); | |
| await github.rest.repos.deleteReleaseAsset({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| asset_id: asset.id | |
| }); | |
| } | |
| } | |
| } | |
| - name: Upload Release Asset (ZIP Archive) | |
| if: (github.event_name == 'release') || (github.event_name == 'workflow_dispatch' && steps.check_release.outputs.exists == 'true') | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: | | |
| dist/VCM-${{ steps.get_version.outputs.VERSION }}-x64.zip | |
| tag_name: ${{ steps.get_version.outputs.TAG }} | |
| # fail_on_unmatched_files: true # Optional: fail if files glob doesn't find anything | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |