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
5 changes: 5 additions & 0 deletions .changeset/backport-npm-trusted-publishing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openchoreo/backstage-plugin': patch
---

Publish this release line to the public npm registry instead of GitHub Packages, using npm trusted publishing (OIDC). Installing no longer requires a GitHub personal access token.
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@openchoreo/cell-diagram"
]
],
"access": "restricted",
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["app", "backend"]
Expand Down
238 changes: 204 additions & 34 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,49 +13,81 @@ env:
IMAGE_NAME: openchoreo/openchoreo-ui

jobs:
release:
name: Release
# Backported from main (see the 1.3.0 cutover). Deliberately does NOT bump
# Yarn: publishing goes through `yarn pack` + `npm publish`, and 4.4.1 packs
# identically, so this release line keeps its pinned toolchain.
#
# Build and publish are deliberately separate jobs.
#
# `yarn install` executes lifecycle scripts from the full transitive
# dependency tree (this repo needs them — see enableScripts in .yarnrc.yml).
# If that ran in the job holding `id-token: write`, any compromised
# dependency could mint an npm publish credential from the OIDC endpoint and
# ship arbitrary code as @openchoreo. The `npm-publish` environment approval
# does not help: it gates job start, so scripts still run after approval.
#
# So: `build` installs and compiles with no OIDC access and emits tarballs;
# `publish-npm` holds the OIDC token but never installs dependencies.
build:
name: Build packages
runs-on: ubuntu-24.04
permissions:
packages: write
contents: read
outputs:
release-tag: ${{ steps.release-vars.outputs.release-tag }}
git-sha-short: ${{ steps.release-vars.outputs.git-sha-short }}
is-prerelease: ${{ steps.release-vars.outputs.is-prerelease }}
latest: ${{ steps.release-vars.outputs.latest }}
dist-tag: ${{ steps.release-vars.outputs.dist-tag }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Set commit SHA and release tag
- name: Resolve release variables
id: release-vars
run: |
echo "GIT_SHA_SHORT=$(git rev-parse --short=8 HEAD)" >> $GITHUB_ENV
git fetch --tags --force
GIT_SHA_SHORT=$(git rev-parse --short=8 HEAD)
RELEASE_TAG=${GITHUB_REF##*/}
echo "RELEASE_TAG=${RELEASE_TAG}" >> $GITHUB_ENV

# Detect prerelease versions (SemVer tags containing a hyphen, e.g., v1.0.0-rc.1)
if [[ "${RELEASE_TAG}" == *-* ]]; then
echo "IS_PRERELEASE=true" >> $GITHUB_ENV
IS_PRERELEASE=true
else
echo "IS_PRERELEASE=false" >> $GITHUB_ENV
IS_PRERELEASE=false
fi

- name: Set latest flag
run: |
git fetch --tags --force
latest_tag=$(git tag -l | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1)
if [ "$latest_tag" == "${RELEASE_TAG}" ]; then
echo "LATEST=true" >> $GITHUB_ENV
LATEST=true
else
echo "LATEST=false" >> $GITHUB_ENV
LATEST=false
fi

# npm publish runs before any Docker retag so a publish failure aborts
# the release before GHCR is mutated. The reverse ordering would leave
# the image retagged to vX.Y.Z (and possibly `latest`) without the
# corresponding @openchoreo/* packages on the npm registry.
# Dist-tag scheme: `latest` only for the highest stable version,
# `next` for prereleases, `release-X.Y` for back-line patches so a
# re-release of an older line never steals `latest` from a newer one.
DIST_TAG=latest
if [ "${IS_PRERELEASE}" = "true" ]; then
DIST_TAG=next
elif [ "${LATEST}" != "true" ]; then
VERSION=${RELEASE_TAG#v}
DIST_TAG="release-${VERSION%.*}"
fi

{
echo "release-tag=${RELEASE_TAG}"
echo "git-sha-short=${GIT_SHA_SHORT}"
echo "is-prerelease=${IS_PRERELEASE}"
echo "latest=${LATEST}"
echo "dist-tag=${DIST_TAG}"
} >> $GITHUB_OUTPUT

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: 'yarn'
# Intentionally no `registry-url:` — Yarn Berry ignores the .npmrc
# this would write. Auth is wired via YARN_NPM_AUTH_TOKEN below.

- name: Install dependencies
run: yarn install --immutable
Expand All @@ -68,26 +100,164 @@ jobs:
run: yarn tsc

- name: Build all workspaces
# `yarn npm publish` runs each workspace's `prepack` (which invokes
# `backstage-cli package prepack` to populate `dist/` entry points)
# and rewrites `workspace:^` deps to concrete versions at pack time.
# `npm publish` (and `changeset publish`, which shells out to it for
# non-pnpm repos) leaks `workspace:^` strings into the tarball, so we
# use Yarn Berry's native publish instead. Build first so `dist/`
# exists before prepack reads it.
run: yarn build:all

- name: Publish to GitHub Packages
env:
YARN_NPM_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Record topological publish order
# Dependencies must be published before dependents so a consumer
# installing mid-release never sees a package whose dependency is not
# on the registry yet. `yarn pack` loses that ordering, so capture it
# while the workspace graph is still available.
run: |
TAG=latest
if [ "${IS_PRERELEASE}" = "true" ]; then
TAG=next
: > publish-order.txt
yarn workspaces foreach --all --no-private --topological exec \
bash -c 'printf "%s\n" "${PWD#"$GITHUB_WORKSPACE"/}" >> "$GITHUB_WORKSPACE/publish-order.txt"'
echo "Publish order:"
cat publish-order.txt

- name: Pack workspaces
# `yarn pack` runs each workspace's `prepack` and rewrites `workspace:^`
# deps to concrete versions. `npm pack` does not, and would leak
# `workspace:^` strings into the tarballs and break installs for
# external consumers. Packing here (not publishing) is what lets the
# publish job skip `yarn install` entirely.
run: yarn workspaces foreach --all --no-private --topological --verbose pack

- name: 'Verify no workspace: leaks in tarballs'
# A `workspace:` specifier reaching the registry is unrecoverable
# without a version bump, so fail the release before anything ships.
run: |
status=0
while read -r loc; do
leaks=$(tar -xzOf "${loc}/package.tgz" package/package.json |
node -e '
let s = "";
process.stdin.on("data", d => (s += d)).on("end", () => {
const m = JSON.parse(s);
const bad = [];
for (const field of ["dependencies", "peerDependencies", "optionalDependencies"]) {
for (const [k, v] of Object.entries(m[field] || {})) {
if (String(v).startsWith("workspace:")) bad.push(`${field}.${k}=${v}`);
}
}
if (bad.length) console.log(bad.join(" "));
});
')
if [ -n "$leaks" ]; then
echo "::error::${loc} leaks workspace: specifiers -> ${leaks}"
status=1
fi
done < publish-order.txt
[ "$status" -eq 0 ] && echo "No workspace: leaks found."
exit $status

- name: Upload package tarballs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: npm-tarballs
path: |
publish-order.txt
packages/*/package.tgz
plugins/*/package.tgz
retention-days: 7
if-no-files-found: error

publish-npm:
name: Publish to npm
needs: build
runs-on: ubuntu-24.04
# Bound to a protected environment with required reviewers. npm matches the
# OIDC claim against repository + workflow filename + THIS environment name,
# so renaming it here also requires updating every package's trusted
# publisher (`npm trust github @openchoreo/<pkg> --environment ...`).
environment: npm-publish
permissions:
# Deliberately minimal. No `contents: read` — this job does not check out
# the repository; it only consumes the tarballs built above.
id-token: write
steps:
- name: Download package tarballs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: npm-tarballs

- name: Setup Node.js
# Node 24 (LTS) bundles npm 11.19.0. Node 22 bundles npm 10.x, which
# predates OIDC support — on npm < 11.5.1 the registry treats the
# publish as anonymous and returns a misleading 404 rather than an auth
# error. Using a Node line that already ships a capable npm avoids
# installing any registry-resolved tooling inside this job: nothing
# executable is fetched from the npm registry while the OIDC token is
# reachable. Node itself comes from nodejs.org via setup-node.
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24

- name: Verify npm supports trusted publishing
# Fail loudly here rather than as a confusing 404 at publish time if a
# future Node 24.x ever ships an npm below the OIDC floor.
run: |
MIN=11.5.1
NPM_VERSION=$(npm --version)
echo "npm ${NPM_VERSION} (minimum ${MIN})"
if [ "$(printf '%s\n' "$MIN" "$NPM_VERSION" | sort -V | head -n1)" != "$MIN" ]; then
echo "::error::npm ${NPM_VERSION} is older than ${MIN}; trusted publishing would fail as a 404."
exit 1
fi
yarn workspaces foreach --all --no-private --topological --verbose \
npm publish --tolerate-republish --access public --tag "${TAG}"

- name: Publish to npm
# Auth is npm trusted publishing (OIDC) — there is deliberately no
# NPM_TOKEN here. npm exchanges the workflow's id-token for a
# short-lived registry credential and attaches a provenance attestation.
#
# No `yarn install` runs in this job, so no dependency lifecycle script
# ever executes while the OIDC endpoint is reachable.
env:
DIST_TAG: ${{ needs.build.outputs.dist-tag }}
run: |
published=0
skipped=0
while read -r loc; do
tgz="${loc}/package.tgz"
spec=$(tar -xzOf "${tgz}" package/package.json |
node -e '
let s = "";
process.stdin.on("data", d => (s += d)).on("end", () => {
const m = JSON.parse(s);
process.stdout.write(`${m.name}@${m.version}`);
});
')

# Replaces yarn's --tolerate-republish: re-running a tag after a
# partial failure must skip what already landed rather than abort.
if npm view "${spec}" version --registry https://registry.npmjs.org >/dev/null 2>&1; then
echo "skip ${spec} (already on registry)"
skipped=$((skipped + 1))
continue
fi

echo "publish ${spec} --tag ${DIST_TAG}"
npm publish "${tgz}" \
--access public \
--tag "${DIST_TAG}" \
--provenance \
--registry https://registry.npmjs.org
published=$((published + 1))
done < publish-order.txt

echo "Published ${published}, skipped ${skipped}."

retag-image:
name: Retag release image
needs: [build, publish-npm]
runs-on: ubuntu-24.04
permissions:
packages: write
env:
RELEASE_TAG: ${{ needs.build.outputs.release-tag }}
GIT_SHA_SHORT: ${{ needs.build.outputs.git-sha-short }}
IS_PRERELEASE: ${{ needs.build.outputs.is-prerelease }}
LATEST: ${{ needs.build.outputs.latest }}
steps:
- name: Login to GitHub container registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
Expand Down
2 changes: 0 additions & 2 deletions .npmrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@
# Only enable scripts for trusted, vetted packages when absolutely necessary
# See: https://docs.npmjs.com/cli/v10/using-npm/scripts#life-cycle-scripts
ignore-scripts=true
//npm.pkg.github.com/:_authToken=${YARN_NPM_AUTH_TOKEN}
@openchoreo:registry=https://npm.pkg.github.com
15 changes: 11 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,11 @@ The `Changeset Check` workflow ([`.github/workflows/changeset-check.yml`](.githu

## Releasing

Releases are tag-driven. Pushing a `v*.*.*` tag triggers the [release workflow](.github/workflows/release.yml), which retags the Docker image in GHCR **and** publishes every public `@openchoreo/*` package to GitHub Packages (`https://npm.pkg.github.com`). Authentication uses the auto-issued `GITHUB_TOKEN` — no extra secrets needed.
Releases are tag-driven. Pushing a `v*.*.*` tag triggers the [release workflow](.github/workflows/release.yml), which publishes every public `@openchoreo/*` package to the **public npm registry** and then retags the Docker image in GHCR.

**There are no publish secrets.** Authentication is [npm trusted publishing](https://docs.npmjs.com/trusted-publishers): the `publish-npm` job mints a short-lived OIDC token that npm exchanges for a publish credential, accepted only from `openchoreo/backstage-plugins`, from `release.yml`, in the `npm-publish` environment. That job deliberately does not check out the repository or install dependencies — `build` does that without OIDC access and hands over tarballs — so no dependency lifecycle script runs while the token is reachable.

On this release line a stable `vX.Y.Z` publishes under the `release-1.2` dist-tag once a newer line holds `latest`.

### Cutting a release

Expand Down Expand Up @@ -133,7 +137,8 @@ Releases are tag-driven. Pushing a `v*.*.*` tag triggers the [release workflow](

5. **CI publishes**. The release workflow:
- Retags the existing Docker image (built earlier on the `main` push) to `vX.Y.Z` in GHCR.
- Runs `yarn install --immutable && yarn tsc && yarn build:all`, then `yarn workspaces foreach --all --no-private --topological --verbose npm publish --tolerate-republish --access public --tag <latest|next>` to publish npm packages to GitHub Packages.
- `build` (no OIDC access) runs `yarn install --immutable && yarn tsc && yarn build:all`, packs every public workspace with `yarn pack`, fails if any tarball still contains a `workspace:` specifier, and uploads the tarballs.
- `publish-npm` (holds `id-token: write`) downloads them and runs `npm publish <tarball> --access public --provenance`, after a required reviewer approves the `npm-publish` environment.
- On **stable** tags (`vX.Y.Z`) publishes under the `latest` npm dist-tag.
- On **prerelease** tags (`vX.Y.Z-rc.N`, `vX.Y.Z-test.N`, etc. — any tag containing a hyphen) publishes under the `next` dist-tag, leaving `latest` untouched.

Expand All @@ -142,8 +147,10 @@ Releases are tag-driven. Pushing a `v*.*.*` tag triggers the [release workflow](
### Verifying a release

```bash
yarn npm info @openchoreo/backstage-plugin --registry=https://npm.pkg.github.com
yarn npm info @openchoreo/backstage-design-system --registry=https://npm.pkg.github.com
VERSION=1.2.6 # the version just released, without the leading v

yarn npm info "@openchoreo/backstage-plugin@${VERSION}"
npm view "@openchoreo/backstage-plugin@${VERSION}" dist.attestations
```

Both should show the new version. Confirm under `dist-tags` that stable releases moved `latest` and prereleases moved `next`. To confirm `workspace:^` rewriting worked, inspect the `dependencies` field of any published `@openchoreo/*` package — every version specifier should be a concrete range (e.g. `^1.1.0`), never `workspace:^`.
Expand Down
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,25 +214,37 @@ For day-to-day development commands (test, lint, build, plugin development workf

- **`@openchoreo/backstage-plugin`** - Frontend UI components
- **`@openchoreo/backstage-plugin-backend`** - Backend API services
- **`@openchoreo/backstage-plugin-api`** - Shared API client library
- **`@openchoreo/backstage-plugin-common`** - Shared types and API client
- **`@openchoreo/backstage-plugin-react`** - Shared React components and hooks
- **`@openchoreo/backstage-design-system`** - Design system primitives
- **`@openchoreo/backstage-plugin-catalog-backend-module`** - Catalog entity provider
- **`@openchoreo/backstage-plugin-scaffolder-backend-module`** - Scaffolder actions

## Installation

The plugins are published to GitHub Packages. To install them in your Backstage application:
The plugins are published to the public npm registry under the [`@openchoreo`](https://www.npmjs.com/org/openchoreo) scope. No registry configuration or authentication is required.

In your **app** workspace:

```bash
# Configure npm to use GitHub Packages for @openchoreo scope
echo "@openchoreo:registry=https://npm.pkg.github.com" >> .npmrc
yarn workspace app add \
@openchoreo/backstage-design-system \
@openchoreo/backstage-plugin-common \
@openchoreo/backstage-plugin-react \
@openchoreo/backstage-plugin
```

In your **backend** workspace:

# Install the plugins you need
yarn add @openchoreo/backstage-plugin
yarn add @openchoreo/backstage-plugin-backend
yarn add @openchoreo/backstage-plugin-api
```bash
yarn workspace backend add \
@openchoreo/backstage-plugin-backend \
@openchoreo/backstage-plugin-catalog-backend-module
```

Note: You'll need a GitHub personal access token with `packages:read` permission to install from GitHub Packages.
See the [installation guide](https://openchoreo.dev/docs/platform-engineer-guide/backstage-plugins/installing-into-existing-backstage/) for the full wiring.

Releases from `1.2.6` onward on this line are published from CI using [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) and carry a signed [provenance attestation](https://docs.npmjs.com/generating-provenance-statements). Versions `1.1.0` through `1.2.5` were migrated from GitHub Packages and predate trusted publishing, so they have no attestation.

## Feature Flags

Expand Down
3 changes: 1 addition & 2 deletions packages/cell-diagram/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts",
"registry": "https://npm.pkg.github.com"
"types": "dist/index.d.ts"
},
"backstage": {
"role": "web-library"
Expand Down
Loading
Loading