Skip to content
Open
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
120 changes: 110 additions & 10 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,30 @@ name: Package-and-Upload
# https://anshumanfauzdar.medium.com/using-github-actions-to-bundle-python-application-into-a-single-package-and-automatic-release-834bd42e0670

on:
push:
tags:
- '*'
release:
types: [published]
Comment thread
Copilot marked this conversation as resolved.
Resolved
# Manual repair path for a published release. The release tag is resolved from
# the release event, then inputs.release_tag, then github.ref_name when
# dispatched at a tag ref. Don't dispatch for a draft release: the upload step
# finds the release by tag, drafts can't be found that way, and the action then
# creates a second release for the same tag.
# There is deliberately no `push: tags` trigger: release-on-merge.yml pushes a
# tag on every merge to main, and a build should only run once a release is published.
workflow_dispatch:
inputs:
release_tag:
description: 'Release tag to attach binaries to. Required when dispatching from a branch rather than a tag ref.'
required: false
type: string
Comment on lines 21 to +26

concurrency:
group: package-${{ github.event.release.tag_name || inputs.release_tag || github.ref_name }}
cancel-in-progress: false
Comment thread
Copilot marked this conversation as resolved.
Resolved

jobs:

buildexe:
name: Build executables and upload them to the existing release
name: Build executables
Comment on lines 34 to +35
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -97,18 +112,103 @@ jobs:
tar -cvf ${{ matrix.UPLOAD_FILE_NAME }} ${{ matrix.OUT_FILE_NAME }}


# UPLOAD_FILE_NAME distinguishes the two macOS artifacts (x86 vs arm64);
# the shared artifact name `tabcmd-macos` would otherwise collide and each
# upload would clobber the other in the artifact store.
- name: Upload build artifact for ${{ matrix.TARGET }}
uses: actions/upload-artifact@v7
with:
name: tabcmd-${{ matrix.TARGET }}
name: ${{ matrix.UPLOAD_FILE_NAME }}
path: ./dist/${{ matrix.TARGET }}/${{ matrix.UPLOAD_FILE_NAME }}

- name: Upload binaries to release for ${{ matrix.TARGET }}
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
# Attach the built binaries to the target GitHub release. Split into its own
# job so we can gate it behind the `release` environment: a required-reviewer
# protection on that environment means publishing a release triggers a build,
# but only an approved reviewer can actually attach binaries to that release.
# The workflow pauses at this job until a reviewer clicks Approve.
#
# `needs: buildexe` waits for ALL matrix legs to succeed. If any leg fails
# this job is skipped (default behavior with no `if: always()`), so a partial
# release upload where e.g. macOS is missing is never possible.
upload_to_release:
name: Attach build artifacts to release
needs: buildexe
runs-on: ubuntu-latest
environment: release
permissions:
contents: write
steps:
# download-artifact versions independently of upload-artifact; v7 reads the
# zipped artifacts that upload-artifact@v7 produces by default
- name: Download all build artifacts
uses: actions/download-artifact@v7
with:
path: artifacts/

# Resolve the release tag, in order of preference: the release event's
# tag_name, the workflow_dispatch release_tag input, then github.ref_name
# when the run was dispatched from a tag ref. A dispatch from a branch
# with no release_tag fails fast rather than treating the branch name as
# a tag, and an empty result never reaches the upload steps.
- name: Resolve release tag
id: tag
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
INPUT_TAG: ${{ inputs.release_tag }}
REF_NAME: ${{ github.ref_name }}
REF_TYPE: ${{ github.ref_type }}
EVENT_NAME: ${{ github.event_name }}
run: |
TAG="$RELEASE_TAG"
if [ -z "$TAG" ]; then
TAG="$INPUT_TAG"
fi
if [ -z "$TAG" ] && [ "$REF_TYPE" = "tag" ]; then
TAG="$REF_NAME"
fi
if [ -z "$TAG" ] && [ "$EVENT_NAME" = "workflow_dispatch" ]; then
echo "::error::workflow_dispatch from $REF_TYPE '$REF_NAME' has no release_tag input; pass -f release_tag=<tag> or dispatch with --ref <tag>"
exit 1
fi
if [ -z "$TAG" ]; then
echo "::error::No release tag resolvable (event=$EVENT_NAME, release.tag_name empty, release_tag input empty, ref_type=$REF_TYPE)"
exit 1
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Resolved release tag: $TAG"

- name: Upload tabcmd.exe (Windows) to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
asset_name: tabcmd.exe
file: artifacts/tabcmd.exe/tabcmd.exe
tag: ${{ steps.tag.outputs.tag }}
overwrite: true

- name: Upload tabcmd (Ubuntu) to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
asset_name: tabcmd
file: artifacts/tabcmd/tabcmd
tag: ${{ steps.tag.outputs.tag }}
overwrite: true

- name: Upload tabcmd-x86.app.tar (macOS x86) to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
asset_name: tabcmd-x86.app.tar
file: artifacts/tabcmd-x86.app.tar/tabcmd-x86.app.tar
tag: ${{ steps.tag.outputs.tag }}
overwrite: true

- name: Upload tabcmd_arm64.app.tar (macOS ARM64) to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
asset_name: ${{ matrix.UPLOAD_FILE_NAME }}
file: ./dist/${{ matrix.TARGET }}/${{ matrix.UPLOAD_FILE_NAME }}
tag: ${{ github.ref_name }}
asset_name: tabcmd_arm64.app.tar
file: artifacts/tabcmd_arm64.app.tar/tabcmd_arm64.app.tar
tag: ${{ steps.tag.outputs.tag }}
overwrite: true
13 changes: 3 additions & 10 deletions .github/workflows/release-on-merge.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
name: Draft release on merge to main

# Triggers when a PR is merged into main (push to main covers all merge strategies).
# Creates a draft GitHub release with auto-generated notes, pushes the version tag,
# and explicitly dispatches package.yml to build and attach binaries.
# (Tag pushes from GITHUB_TOKEN do not trigger other workflows, so we dispatch explicitly.)
# When you publish the draft release, publish-pypi.yml uploads to PyPI automatically.
# Pushes the next patch version tag and creates a draft GitHub release with auto-generated notes.
# Nothing is built here. When you publish the draft release, package.yml builds and attaches
# the binaries and publish-pypi.yml uploads to PyPI.

on:
push:
Expand Down Expand Up @@ -59,9 +58,3 @@ jobs:
--title "$TAG" \
--generate-notes \
--notes-start-tag "$LAST_TAG"

- name: Trigger binary builds
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.version.outputs.tag }}
run: gh workflow run package.yml --ref "$TAG"
42 changes: 30 additions & 12 deletions contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* [Releases](#releases)
* [Versioning](#versioning)
* [Packaging](#packaging)
* [Publishing a release](#publishing-a-release)


## Install Tabcmd
Expand Down Expand Up @@ -176,26 +177,43 @@ To run the newly created executable, from a console window in the same directory
To investigate what's packaged in the executable, use https://pyinstxtractor-web.netlify.app/


### Publishing a release

Merging into `main` triggers `release-on-merge.yml`, which pushes the next patch version tag and creates a **draft** release for it. Nothing is built at this point.

When you then **publish** that draft release in the GitHub UI:

1. `package.yml` fires on the `release: published` event and builds the binaries against the tagged commit.
2. Before binaries are attached, the workflow **pauses at the `upload_to_release` job** waiting for a reviewer to approve the `release` environment. Go to **Actions → the latest `Package-and-Upload` run → Review deployments → Approve**. The four binaries (Windows `.exe`, Ubuntu `tabcmd`, macOS x86 and arm64 `.app.tar`) get attached after approval.
3. Separately, `publish-pypi.yml` uploads the wheel to PyPI, gated on the `pypi` environment.

If binaries need to be re-attached to a published release (e.g. one build leg originally failed), dispatch the workflow at the target tag. This doesn't work for a draft release: the upload step looks the release up by tag, and GitHub doesn't return drafts by tag.

```shell
gh workflow run package.yml --ref v2.1.0
```

To dispatch from a branch (e.g. to test workflow changes), pass the release to attach to: `gh workflow run package.yml --ref <branch> -f release_tag=v2.1.0`. A branch dispatch without `release_tag` fails at the tag-resolution step.

Same reviewer gate applies.


## Release process

1. Create a new Github project release manually: https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases
Releases are created and published as described in [Publishing a release](#publishing-a-release). The manual steps are:

1. Edit the draft release created by `release-on-merge.yml` before publishing it:
- include a useful list of changes since the last release
- include a clear list of remaining non-server-admin functional gaps

1. This will trigger our github packaging action to run on 3 different OSs.
1. Publish the release. This triggers `package.yml` and `publish-pypi.yml`:
- write an updated metadata file with the correct version number.
- build the python wheel
- run pyinstaller to create executables
- save the executable as an artifact on that job.

1. Find the artifacts created by this job and manually copy them to the new release.
- manually download. They will all be returned as zips
- unzip the windows.exe and mac.app.tar files and upload those
- do not unzip the linux app, github doesn't like it. upload as tabcmd.zip
(Pay attention to what the file type is, github also sends it as a zip if you download with curl etc. TODO: automate workflow with a github action)
- run pyinstaller to create executables on each OS
- after a reviewer approves the `release` environment, attach the executables to the release.
- upload the wheel to PyPI (after approval of the `pypi` environment).

1. To trigger publishing to pypi run the manual workflow on main with 'pypi'.
1. To publish to PyPI without a release (or to test on Test PyPI), run the manual `Publish to PyPi` workflow on main.

1. When the packages are available on pypi, you can run the 'Pypi smoke test action'. This action will also be run every 24 hours to validate doing pip install. (TODO: automate the after-release trigger)


Loading