Skip to content

Build Release Assets #12

Build Release Assets

Build Release Assets #12

Workflow file for this run

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 }}