Publish Packages #6
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: 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 |