diff --git a/.github/workflows/ts-sdk-release.yml b/.github/workflows/ts-sdk-release.yml new file mode 100644 index 0000000000000..e8b27b8956d4c --- /dev/null +++ b/.github/workflows/ts-sdk-release.yml @@ -0,0 +1,233 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +--- +name: Release TypeScript SDK + +on: # yamllint disable-line rule:truthy + workflow_dispatch: + inputs: + release_type: + description: "Stage the package for review or publish it directly" + required: true + type: choice + options: + - staged + - formal + tag: + description: "TypeScript SDK release tag (ts-sdk/)" + required: true + type: string + npm_tag: + description: "npm dist-tag (for example: beta or latest)" + required: true + type: string + +permissions: + contents: read + +concurrency: + group: ts-sdk-npm-release + cancel-in-progress: false + +jobs: + staged-release: + name: Stage release on npm + if: inputs.release_type == 'staged' + runs-on: ubuntu-latest + environment: ts-sdk-npm + permissions: + contents: read + id-token: write + steps: + - name: Validate release inputs + id: release + env: + NPM_TAG: ${{ inputs.npm_tag }} + RELEASE_TAG: ${{ inputs.tag }} + SEMVER_PATTERN: >- + ^ts-sdk/([0-9]+\.[0-9]+\.[0-9]+ + (-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?)$ + run: | + set -euo pipefail + if [[ ! "${RELEASE_TAG}" =~ ${SEMVER_PATTERN// /} ]]; then + echo "Release tag must have the form ts-sdk/" >&2 + exit 1 + fi + version="${BASH_REMATCH[1]}" + if [[ ! "${NPM_TAG}" =~ ^[0-9A-Za-z][0-9A-Za-z._-]*$ ]]; then + echo "A valid npm_tag is required" >&2 + exit 1 + fi + if [[ "${version}" == *-* && "${NPM_TAG}" == "latest" ]]; then + echo "Prereleases must not use the latest npm dist-tag" >&2 + exit 1 + fi + if [[ "${version}" != *-* && "${NPM_TAG}" != "latest" ]]; then + echo "Stable releases must use the latest npm dist-tag" >&2 + exit 1 + fi + echo "version=${version}" >> "${GITHUB_OUTPUT}" + - name: Checkout release tag + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.tag }} + fetch-depth: 0 + persist-credentials: false + - name: Confirm checkout is the requested tag + env: + RELEASE_TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + test "$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")" = "$(git rev-parse HEAD)" + git merge-base --is-ancestor HEAD "$(git rev-parse refs/remotes/origin/main)" + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24.19.0" + registry-url: "https://registry.npmjs.org" + package-manager-cache: false + - name: Enable release toolchain + run: | + set -euo pipefail + node --input-type=module --eval ' + import { execFileSync } from "node:child_process"; + const npmVersion = execFileSync("npm", ["--version"], { encoding: "utf8" }); + const [major, minor] = npmVersion.split(".").map(Number); + if (major < 11 || (major === 11 && minor < 15)) process.exit(1); + ' + corepack enable + - name: Verify package identity + working-directory: ts-sdk + env: + EXPECTED_VERSION: ${{ steps.release.outputs.version }} + run: | + set -euo pipefail + test "$(node --print "require('./package.json').name")" = "apache-airflow-ts-sdk" + test "$(node --print "require('./package.json').version")" = "${EXPECTED_VERSION}" + - name: Verify and package release + working-directory: ts-sdk + run: | + set -euo pipefail + pnpm install --frozen-lockfile + pnpm run lint + pnpm run format:check + pnpm run typecheck + pnpm test + pnpm run build + npm pack --dry-run + mkdir -p ../package-artifact + npm pack --pack-destination ../package-artifact + - name: Stage with npm trusted publishing + env: + NPM_TAG: ${{ inputs.npm_tag }} + run: >- + npm stage publish package-artifact/*.tgz + --access public --tag "${NPM_TAG}" + + formal-release: + name: Publish formal release to npm + if: inputs.release_type == 'formal' + runs-on: ubuntu-latest + environment: ts-sdk-npm + permissions: + contents: read + id-token: write + steps: + - name: Validate release inputs + id: release + env: + NPM_TAG: ${{ inputs.npm_tag }} + RELEASE_TAG: ${{ inputs.tag }} + SEMVER_PATTERN: >- + ^ts-sdk/([0-9]+\.[0-9]+\.[0-9]+ + (-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?)$ + run: | + set -euo pipefail + if [[ ! "${RELEASE_TAG}" =~ ${SEMVER_PATTERN// /} ]]; then + echo "Release tag must have the form ts-sdk/" >&2 + exit 1 + fi + version="${BASH_REMATCH[1]}" + if [[ ! "${NPM_TAG}" =~ ^[0-9A-Za-z][0-9A-Za-z._-]*$ ]]; then + echo "A valid npm_tag is required" >&2 + exit 1 + fi + if [[ "${version}" == *-* && "${NPM_TAG}" == "latest" ]]; then + echo "Prereleases must not use the latest npm dist-tag" >&2 + exit 1 + fi + if [[ "${version}" != *-* && "${NPM_TAG}" != "latest" ]]; then + echo "Stable releases must use the latest npm dist-tag" >&2 + exit 1 + fi + echo "version=${version}" >> "${GITHUB_OUTPUT}" + - name: Checkout release tag + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.tag }} + fetch-depth: 0 + persist-credentials: false + - name: Confirm checkout is the requested tag + env: + RELEASE_TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + test "$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")" = "$(git rev-parse HEAD)" + git merge-base --is-ancestor HEAD "$(git rev-parse refs/remotes/origin/main)" + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24.19.0" + registry-url: "https://registry.npmjs.org" + package-manager-cache: false + - name: Enable release toolchain + run: | + set -euo pipefail + node --input-type=module --eval ' + import { execFileSync } from "node:child_process"; + const npmVersion = execFileSync("npm", ["--version"], { encoding: "utf8" }); + const [major, minor] = npmVersion.split(".").map(Number); + if (major < 11 || (major === 11 && minor < 15)) process.exit(1); + ' + corepack enable + - name: Verify package identity + working-directory: ts-sdk + env: + EXPECTED_VERSION: ${{ steps.release.outputs.version }} + run: | + set -euo pipefail + test "$(node --print "require('./package.json').name")" = "apache-airflow-ts-sdk" + test "$(node --print "require('./package.json').version")" = "${EXPECTED_VERSION}" + - name: Verify and package release + working-directory: ts-sdk + run: | + set -euo pipefail + pnpm install --frozen-lockfile + pnpm run lint + pnpm run format:check + pnpm run typecheck + pnpm test + pnpm run build + npm pack --dry-run + mkdir -p ../package-artifact + npm pack --pack-destination ../package-artifact + - name: Publish with npm trusted publishing + env: + NPM_TAG: ${{ inputs.npm_tag }} + run: >- + npm publish package-artifact/*.tgz + --access public --tag "${NPM_TAG}" diff --git a/airflow-core/docs/authoring-and-scheduling/language-sdks/typescript.rst b/airflow-core/docs/authoring-and-scheduling/language-sdks/typescript.rst index b93be7b38cefc..84b5bc25398d5 100644 --- a/airflow-core/docs/authoring-and-scheduling/language-sdks/typescript.rst +++ b/airflow-core/docs/authoring-and-scheduling/language-sdks/typescript.rst @@ -26,7 +26,7 @@ The TypeScript SDK lets you implement Airflow task logic in TypeScript (or plain Node.js. The Dag and its scheduling remain in Python; individual tasks delegate to a Node.js subprocess that is spawned by :class:`~airflow.sdk.coordinators.node.NodeCoordinator` for each task instance. -The SDK is an ESM-only package that ships from the ``ts-sdk/`` directory of the Airflow repository. It is currently in **alpha** and its API may change. +The SDK is the ``apache-airflow-ts-sdk`` package (ESM-only). It is currently in **alpha** and its API may change. .. warning:: @@ -93,7 +93,7 @@ entry point. .. code-block:: typescript - import { Dag, DagRegistry, serveDags, type TaskHandlerArgs } from "@apache-airflow/ts-sdk"; + import { Dag, DagRegistry, serveDags, type TaskHandlerArgs } from "apache-airflow-ts-sdk"; export async function buildMessage({ ctx, client }: TaskHandlerArgs) { const upstream = await client.getXCom({ diff --git a/dev/breeze/tests/test_release_management_commands.py b/dev/breeze/tests/test_release_management_commands.py index 0e0d3d6484343..4a94a8af21272 100644 --- a/dev/breeze/tests/test_release_management_commands.py +++ b/dev/breeze/tests/test_release_management_commands.py @@ -311,7 +311,7 @@ def test_get_package_version_possibly_from_stable_txt_for_ts_sdk( monkeypatch.setattr(global_constants, "AIRFLOW_ROOT_PATH", tmp_path) package_json = tmp_path / "ts-sdk" / "package.json" package_json.parent.mkdir(parents=True) - package_json.write_text('{"name": "@apache-airflow/ts-sdk", "version": "0.2.0-alpha.1"}\n') + package_json.write_text('{"name": "apache-airflow-ts-sdk", "version": "0.2.0-alpha.1"}\n') if stable_txt_content is not None: stable_txt = tmp_path / "generated" / "_build" / "docs" / "ts-sdk" / "stable.txt" stable_txt.parent.mkdir(parents=True) diff --git a/ts-sdk/README.md b/ts-sdk/README.md index 45ea2bc31a98a..6bc0dd2a0d546 100644 --- a/ts-sdk/README.md +++ b/ts-sdk/README.md @@ -33,7 +33,7 @@ runtime used to execute registered TypeScript handlers from Airflow. ## Task Handlers ```ts -import { Dag, DagRegistry, serveDags, type TaskHandlerArgs } from "@apache-airflow/ts-sdk"; +import { Dag, DagRegistry, serveDags, type TaskHandlerArgs } from "apache-airflow-ts-sdk"; export async function sayHello({ ctx, client }: TaskHandlerArgs) { const greeting = await client.getVariable("greeting"); @@ -98,7 +98,7 @@ Airflow metadata in the bundle itself. TypeScript entrypoint: ```ts -import { Dag, DagRegistry, serveDags, type TaskHandlerArgs } from "@apache-airflow/ts-sdk"; +import { Dag, DagRegistry, serveDags, type TaskHandlerArgs } from "apache-airflow-ts-sdk"; export async function extract({ client }: TaskHandlerArgs) { const connection = await client.getConnection("sales_db"); @@ -151,7 +151,7 @@ entrypoint that serves them all: ```ts import { salesDag } from "./sales/dag"; import { billingDag } from "./billing/dag"; -import { DagRegistry, serveDags } from "@apache-airflow/ts-sdk"; +import { DagRegistry, serveDags } from "apache-airflow-ts-sdk"; await serveDags(new DagRegistry(salesDag, billingDag)); ``` @@ -271,3 +271,62 @@ gh workflow run "Publish Docs to S3" --repo apache/airflow --ref main \ Use `destination=staging` first to check the output, then `live`. Confirm that `https://airflow.apache.org/docs/ts-sdk/stable/` resolves (allow time for cache invalidation) and that `/docs/ts-sdk/` redirects to it. + +## Publishing + +The manually dispatched `Release TypeScript SDK` workflow has separate jobs +for [npm's staged-publishing flow](https://docs.npmjs.com/staged-publishing/) +and for direct formal publication. Configure the `apache-airflow-ts-sdk` +trusted publisher to permit both operations from the workflow's protected +`ts-sdk-npm` environment: + +```bash +npm trust github apache-airflow-ts-sdk \ + --repo apache/airflow \ + --file ts-sdk-release.yml \ + --environment ts-sdk-npm \ + --allow-stage-publish \ + --allow-publish +``` + +Create a `ts-sdk/` tag whose version exactly matches `package.json`. +To submit the package to npm's private staging area for review, run: + +```bash +gh workflow run ts-sdk-release.yml --repo apache/airflow --ref main \ + -f release_type=staged \ + -f tag=ts-sdk/1.0.0-beta1 \ + -f npm_tag=beta +``` + +The staged job runs the full verification and package build, then calls +`npm stage publish`. The version is not publicly installable until a maintainer +reviews and approves it with 2FA. The following commands require npm 11.15 or +later: + +```bash +npm stage list apache-airflow-ts-sdk +npm stage view +npm stage download +npm stage approve +``` + +The approval cannot run through the trusted-publisher workflow because npm +requires interactive proof of presence. Reject an unsuitable staged version +with `npm stage reject `. Do not run the formal workflow for a version +that is already staged; approve or reject that staged version instead. + +To publish directly without npm's staging review, trigger the formal job: + +```bash +gh workflow run ts-sdk-release.yml --repo apache/airflow --ref main \ + -f release_type=formal \ + -f tag=ts-sdk/1.0.0-beta1 \ + -f npm_tag=beta +``` + +Use `latest` for stable releases and a non-`latest` tag such as `alpha`, +`beta`, or `rc` for prereleases. Both jobs use short-lived npm OIDC credentials +and automatically publish provenance. After verifying the trusted-publisher +setup, disable token-based publishing and revoke obsolete npm automation +tokens. diff --git a/ts-sdk/docs/index.md b/ts-sdk/docs/index.md index edda33fe48dc1..52b41b46ffe7c 100644 --- a/ts-sdk/docs/index.md +++ b/ts-sdk/docs/index.md @@ -40,7 +40,7 @@ the `"return_value"` key by the active runtime, matching Python `@task` behavior: ```ts -import { Dag, DagRegistry, serveDags, type TaskHandlerArgs } from "@apache-airflow/ts-sdk"; +import { Dag, DagRegistry, serveDags, type TaskHandlerArgs } from "apache-airflow-ts-sdk"; export async function sayHello({ ctx, client }: TaskHandlerArgs) { const greeting = await client.getVariable("greeting"); diff --git a/ts-sdk/example/package.json b/ts-sdk/example/package.json index 6569ce6b74ffd..f87d857c0deb0 100644 --- a/ts-sdk/example/package.json +++ b/ts-sdk/example/package.json @@ -1,5 +1,5 @@ { - "name": "@apache-airflow/ts-sdk-example", + "name": "apache-airflow-ts-sdk-example", "private": true, "version": "0.0.0", "type": "module", @@ -9,7 +9,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@apache-airflow/ts-sdk": "file:.." + "apache-airflow-ts-sdk": "file:.." }, "devDependencies": { "@types/node": "^26.1.2", diff --git a/ts-sdk/example/src/main.ts b/ts-sdk/example/src/main.ts index d08530615e57f..563f474b422a2 100644 --- a/ts-sdk/example/src/main.ts +++ b/ts-sdk/example/src/main.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Dag, DagRegistry, serveDags, type TaskHandlerArgs } from "@apache-airflow/ts-sdk"; +import { Dag, DagRegistry, serveDags, type TaskHandlerArgs } from "apache-airflow-ts-sdk"; const dag = new Dag("typescript_example"); diff --git a/ts-sdk/package.json b/ts-sdk/package.json index da1e4dea5d0b6..f93b640973180 100644 --- a/ts-sdk/package.json +++ b/ts-sdk/package.json @@ -1,5 +1,5 @@ { - "name": "@apache-airflow/ts-sdk", + "name": "apache-airflow-ts-sdk", "version": "0.1.0-alpha.0", "packageManager": "pnpm@10.28.1", "description": "TypeScript Task SDK for Apache Airflow task handlers", diff --git a/ts-sdk/pnpm-lock.yaml b/ts-sdk/pnpm-lock.yaml index 00c9113e8e1b0..d6717944c6208 100644 --- a/ts-sdk/pnpm-lock.yaml +++ b/ts-sdk/pnpm-lock.yaml @@ -45,7 +45,7 @@ importers: example: dependencies: - '@apache-airflow/ts-sdk': + apache-airflow-ts-sdk: specifier: file:.. version: file:(esbuild@0.28.2) devDependencies: @@ -61,7 +61,7 @@ importers: packages: - '@apache-airflow/ts-sdk@file:': + 'apache-airflow-ts-sdk@file:': resolution: {directory: '', type: directory} engines: {node: '>=22'} hasBin: true @@ -1340,7 +1340,7 @@ packages: snapshots: - '@apache-airflow/ts-sdk@file:(esbuild@0.28.2)': + 'apache-airflow-ts-sdk@file:(esbuild@0.28.2)': dependencies: '@msgpack/msgpack': 3.1.3 optionalDependencies: diff --git a/ts-sdk/src/sdk/brand.ts b/ts-sdk/src/sdk/brand.ts index b9fab223862f4..a785f736c02b9 100644 --- a/ts-sdk/src/sdk/brand.ts +++ b/ts-sdk/src/sdk/brand.ts @@ -40,4 +40,4 @@ export function hasBrand(value: unknown, name: string): boolean { /** Tail shared by the errors reporting a second resolved copy. */ export const DUPLICATE_COPY_HINT = - "comes from a different copy of @apache-airflow/ts-sdk; deduplicate the dependency so one copy is resolved"; + "comes from a different copy of apache-airflow-ts-sdk; deduplicate the dependency so one copy is resolved";