Skip to content

Publish Packages

Publish Packages #6

Workflow file for this run

name: Publish Packages
# Both registries use OIDC trusted publishing, which must be configured once
# on pypi.org and npmjs.com for this repo, workflow file and environment.
on:
release:
types: [published]
workflow_dispatch:
inputs:
target:
description: 'Which package(s) to publish'
type: choice
options: [all, pypi, npm, go]
default: all
dry_run:
description: 'Build and validate only, without uploading'
type: boolean
default: true
permissions:
contents: read
jobs:
verify-versions:
runs-on: ubuntu-latest
outputs:
# Read by tag-go-module when there is no release tag to take it from.
go: ${{ steps.versions.outputs.go }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Read declared versions
id: versions
run: |
py_version=$(grep -oP '__version__\s*=\s*"\K[^"]+' python/FlightRadarAPI/__init__.py)
npm_version=$(node -p "require('./nodejs/package.json').version")
go_version=$(grep -oP 'const Version = "\K[^"]+' go/flightradarapi/doc.go)
echo "python=$py_version" >> "$GITHUB_OUTPUT"
echo "npm=$npm_version" >> "$GITHUB_OUTPUT"
echo "go=$go_version" >> "$GITHUB_OUTPUT"
echo "Python: $py_version | npm: $npm_version | Go: $go_version"
- name: Check versions are in sync
# The Go module has no registry upload below: the tag is its release,
# and this check is what keeps its version honest.
run: |
if [ "${{ steps.versions.outputs.python }}" != "${{ steps.versions.outputs.npm }}" ] \
|| [ "${{ steps.versions.outputs.python }}" != "${{ steps.versions.outputs.go }}" ]; then
echo "::error::Version mismatch: python=${{ steps.versions.outputs.python }} npm=${{ steps.versions.outputs.npm }} go=${{ steps.versions.outputs.go }}"
exit 1
fi
- name: Check versions match the release tag
if: github.event_name == 'release'
run: |
tag="${{ github.event.release.tag_name }}"
tag="${tag#v}"
if [ "$tag" != "${{ steps.versions.outputs.python }}" ]; then
echo "::error::Release tag $tag does not match package version ${{ steps.versions.outputs.python }}"
exit 1
fi
tag-go-module:
needs: [verify-versions, publish-pypi, publish-npm]
# Last on purpose: the tag is the only step here that cannot be undone, and
# a registry that rejected the version must not be outlived by a module the
# proxy serves forever. A single-target dispatch skips the two publish jobs,
# and a skipped dependency would take this job with it — which is what the
# cancelled/failure pair is there to prevent.
if: >-
!cancelled() && !failure()
&& (github.event_name == 'release' || inputs.target == 'all' || inputs.target == 'go')
runs-on: ubuntu-latest
permissions:
contents: write # Required to push the module tag
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
# A module in a subdirectory is released by a tag carrying that prefix, so
# `go get .../go@latest` sees nothing without this.
#
# Run from a release, the tag points at the release's own tag. Run by hand
# it points at the dispatched commit, which is what releasing the Go
# module on its own means: the other two packages did not change, so there
# is no release tag to hang it from.
- name: Tag the Go module
env:
EVENT_NAME: ${{ github.event_name }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
DECLARED_VERSION: ${{ needs.verify-versions.outputs.go }}
DRY_RUN: ${{ github.event_name != 'release' && inputs.dry_run }}
run: |
# Reports in a dry run and refuses for real: a rehearsal has nothing
# to undo, so it must say what would happen rather than fail. Without
# this, the only way to try a dispatch change — from its own branch —
# dies at the first guard.
refuse() {
if [ "${DRY_RUN}" != "false" ]; then
echo "Would refuse: $1"
exit 0
fi
echo "::error::$1"
exit 1
}
if [ "${EVENT_NAME}" = "release" ]; then
version="${RELEASE_TAG#v}"
target="${RELEASE_TAG}"
else
version="${DECLARED_VERSION}"
target="${GITHUB_SHA}"
fi
module_tag="go/v${version}"
commit=$(git rev-parse -q --verify "${target}^{commit}") ||
refuse "${target} does not resolve to a commit in this checkout"
# Idempotent on the commit, not on the name: re-running a release, or
# publishing one after the module was tagged by hand, must not fail.
if existing=$(git rev-parse -q --verify "refs/tags/${module_tag}^{commit}"); then
if [ "${existing}" = "${commit}" ]; then
echo "${module_tag} already points at ${commit}"
exit 0
fi
refuse "${module_tag} exists at ${existing}, not ${commit} — a published version is immutable, bump go/flightradarapi/doc.go"
fi
# The tag has to carry the module, or the proxy caches an empty one.
git cat-file -e "${commit}:go/go.mod" 2>/dev/null ||
refuse "${target} carries no go/go.mod"
# A dispatch can be started from any branch, and the tag it leaves
# behind cannot be taken back.
if [ "${EVENT_NAME}" != "release" ]; then
git fetch -q origin main ||
refuse "could not read main to check the commit against it"
# Told apart deliberately: 1 means "not an ancestor", anything else
# is git failing, which must not read as an accusation about the
# commit. FETCH_HEAD, because the line above is what just wrote it.
status=0
git merge-base --is-ancestor "${commit}" FETCH_HEAD || status=$?
case "${status}" in
0) ;;
1) refuse "${commit} is not on main" ;;
*) refuse "could not compare ${commit} against main (git exited ${status})" ;;
esac
fi
if [ "${DRY_RUN}" != "false" ]; then
echo "Would tag ${module_tag} at ${commit}"
exit 0
fi
git tag "${module_tag}" "${commit}"
git push origin "${module_tag}"
echo "Pushed ${module_tag} at ${commit}"
publish-pypi:
needs: verify-versions
if: github.event_name == 'release' || inputs.target == 'all' || inputs.target == 'pypi'
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # Required for trusted publishing (OIDC)
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.13'
- name: Install build tools
run: python -m pip install --upgrade pip build twine
- name: Build package
run: python -m build ./python
- name: Validate distributions
run: python -m twine check python/dist/*
- name: Verify the built wheel imports
run: |
pip install python/dist/*.whl
python -c "from FlightRadarAPI import FlightRadar24API; FlightRadar24API(); print('Install OK')"
- name: Publish to PyPI
if: github.event_name == 'release' || inputs.dry_run == false
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: python/dist/
publish-npm:
needs: verify-versions
if: github.event_name == 'release' || inputs.target == 'all' || inputs.target == 'npm'
runs-on: ubuntu-latest
environment: npm
permissions:
id-token: write # Required for trusted publishing (OIDC) + provenance
defaults:
run:
working-directory: ./nodejs
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Set up NodeJS
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22.x'
registry-url: 'https://registry.npmjs.org'
- name: Upgrade npm
# npm trusted publishing (OIDC) requires npm >= 11.5.1.
run: npm install -g npm@latest
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npm run test:types
- name: Offline tests
# Live FR24 integration tests are left out on purpose: a flaky upstream
# response must not block a release that already passed CI.
run: npm run test:offline
- name: Validate package contents
run: npm publish --dry-run
- name: Publish to npm
if: github.event_name == 'release' || inputs.dry_run == false
run: npm publish --provenance --access public