Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
branches: [main]
pull_request:
branches: [main]
# The release runs this same gate on the commit it releases.
workflow_call:

permissions:
contents: read
Expand Down
147 changes: 147 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
name: Release

# Run by hand from the Actions tab, on main, once CI is green there. See
# apps/docs/src/content/contributing/publishing.md.
on:
workflow_dispatch:
inputs:
version:
description: 'An exact version (0.1.0), or a bump: patch, or minor for a breaking change'
required: true
default: patch

# One release at a time: two would race for the same version.
concurrency:
group: release
cancel-in-progress: false

permissions:
# The version commit, the tag and the GitHub release.
contents: write
# npm provenance and trusted publishing.
id-token: write

jobs:
# The same gate as every push to main, on exactly the commit being released.
ci:
if: github.ref == 'refs/heads/main'
uses: ./.github/workflows/ci.yml
permissions:
contents: read
actions: read

release:
needs: ci
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

- uses: ./.github/actions/setup
with:
registry-url: https://registry.npmjs.org

- name: Version
id: version
env:
INPUT: ${{ inputs.version }}
working-directory: packages/devtools
run: |
if [[ ! "$INPUT" =~ ^(patch|minor|[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
echo "::error::version must be patch, minor or an exact x.y.z, not '$INPUT'"
exit 1
fi
IFS=. read -r major minor patch <<< "$(node -p "require('./package.json').version")"
case "$INPUT" in
patch) version="$major.$minor.$((patch + 1))" ;;
minor) version="$major.$((minor + 1)).0" ;;
# The same version as now is allowed: running again with the exact version of a
# release that failed part way through finishes it.
*) version="$INPUT" ;;
esac
# Only the version line changes. `npm version` would rewrite the whole file, and turn
# the escaped characters in the description into literal ones.
sed -i -E "s/^( \"version\": )\"[^\"]+\"/\1\"$version\"/" package.json
test "$(node -p "require('./package.json').version")" = "$version"
echo "version=$version" >> "$GITHUB_OUTPUT"

- name: Check the changelog
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
mkdir -p .release
file=packages/devtools/CHANGELOG.md
# This version's section, from its `## x.y.z - date` heading (brackets allowed) to the
# next `## ` heading.
if [ -f "$file" ]; then
awk -v v="$VERSION" '
/^## / {
h = substr($0, 4); sub(/^\[/, "", h); split(h, w, /[] ]/)
inside = (w[1] == v); next
}
inside' "$file" > .release/notes.md
fi
if [ ! -s .release/notes.md ]; then
echo "::error file=$file::Add a '## $VERSION' section to $file before releasing."
exit 1
fi

# `pnpm pack` applies `publishConfig.exports`, and its `prepack` builds the library and the
# panel. `npm publish` of the tarball is what speaks npm's OIDC for trusted publishing.
- name: Build
working-directory: packages/devtools
run: pnpm pack --pack-destination "$GITHUB_WORKSPACE/.release"

# The exact tarball, installed from a local registry into a fresh Angular CLI app and built.
- name: Verify the tarball
run: pnpm verify:publish --tarball="$(ls .release/*.tgz)"

# No token: the package's trusted publisher on npmjs.com names this workflow, and npm
# authenticates the publish through OIDC.
- name: Publish
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
# Already there means an earlier run got this far: skip it, so running again with the
# same exact version finishes a release that failed part way through.
if npm view "@pangular-inspector/devtools@$VERSION" version > /dev/null 2>&1; then
echo "already published: $VERSION"
else
npm publish .release/*.tgz --provenance --access public
fi

- name: Push the version commit and tag
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
tag="devtools@$VERSION"
if ! git diff --quiet -- packages/devtools/package.json; then
git commit -m "chore(release): $VERSION" -- packages/devtools/package.json
fi
# Pull requests can merge while a release runs. Replay the version commit on top of them
# and tag where it lands. A rebase conflict fails the step rather than guessing.
for attempt in 1 2 3; do
git fetch origin main
git rebase origin/main
git tag -f -a "$tag" -m "$tag"
if git push origin HEAD:main; then break; fi
if [ "$attempt" = 3 ]; then exit 1; fi
done
git push -f origin "refs/tags/$tag"

- name: GitHub release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.version.outputs.version }}
run: |
tag="devtools@$VERSION"
if gh release view "$tag" > /dev/null 2>&1; then
echo "release $tag already exists"
else
gh release create "$tag" --title "$VERSION" --notes-file .release/notes.md
fi
87 changes: 81 additions & 6 deletions apps/docs/src/content/contributing/publishing.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
title: Publishing
description: Bump the version, build, and publish the npm package. Ship the Chrome extension with a fresh UI.
description: Release the npm package from GitHub Actions, check it from a local registry, and ship the Chrome extension with a fresh UI.
---

<ngmd-hero title="Publishing" gradient>
One npm package and one Chrome extension, each with its own version. Bump, build, publish.
One npm package and one Chrome extension, each with its own version. The package releases from a workflow; the extension ships by hand.
</ngmd-hero>

# Publishing
Expand Down Expand Up @@ -43,20 +43,95 @@ The package's `build` script runs two steps:

`prepack` runs `pnpm build`, so every publish builds first.

## Publish the npm package
## Release from GitHub Actions

The **Release** workflow (`.github/workflows/release.yml`) publishes the package from `main`. It runs by hand from the Actions tab, and publishes through npm trusted publishing, so no npm token is stored anywhere.

### Set it up once

<ngmd-workflow>
<ngmd-step title="Add the trusted publisher">
On npmjs.com, open the settings of <code>&#64;pangular-inspector/devtools</code>, and under <strong>Trusted publishing</strong> choose GitHub Actions with owner <code>pangular-inspector</code>, repository <code>devtools</code> and workflow <code>release.yml</code>. Leave the environment empty.
</ngmd-step>
<ngmd-step title="Let the workflow push to main">
The workflow pushes the version commit and the tag with the default <code>GITHUB_TOKEN</code>. If a branch protection rule or ruleset guards <code>main</code>, allow GitHub Actions to bypass it.
</ngmd-step>
</ngmd-workflow>

### Each release

1. In a pull request, add a section for the version to `packages/devtools/CHANGELOG.md`, headed `## 0.0.8`. The changelog follows [Keep a Changelog](https://keepachangelog.com), with entries grouped as Upgrade notes, Security fixes, Features and Documentation.
2. If `app/` changed since the last release, check that `extension/ui` is current. CI fails when it is stale.
3. Once the pull request is merged and CI is green on `main`, run **Release** from the Actions tab on `main`. Its input is an exact version (`0.0.6`), or `patch`, or `minor` for a breaking change.

### What the workflow does

| Step | What happens |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| CI | Runs `ci.yml` on the commit being released. |
| Version | Sets `version` in `packages/devtools/package.json`. |
| Check the changelog | Fails unless `CHANGELOG.md` has a `## <version> - <date>` section. That section becomes the release notes. |
| Build | `pnpm pack` builds the library and the UI into one tarball, with `publishConfig.exports` applied. |
| Verify the tarball | `pnpm verify:publish` installs that tarball into a fresh Angular CLI app from a local registry, builds it, and checks the hub. See [Check the package](#check-the-package). |
| Publish | `npm publish --provenance` publishes the tarball on the `latest` tag. |
| Push and tag | Commits `chore(release): <version>` to `main` and tags it `devtools@<version>`. |
| GitHub release | Creates a release for the tag, with the changelog section as its notes. |

If the workflow fails before it publishes, nothing has left the runner. Fix the cause and run it again. If it fails after it publishes, run it again with the same exact version (not a bump). It skips the publish when that version is already on npm and finishes the rest.

## Check the package

`pnpm verify:publish` checks what npm gets, which the workspace never uses: it links the package to its TypeScript sources.

<ngmd-workflow>
<ngmd-step title="Pack">
<code>pnpm pack</code> builds the package and checks that the tarball holds every exported file and the UI.
</ngmd-step>
<ngmd-step title="Publish locally">
It starts Verdaccio with <code>pnpm dlx</code> and publishes the tarball there. The registry serves <code>&#64;pangular-inspector/*</code> only from what it was given, and proxies everything else to npmjs.
</ngmd-step>
<ngmd-step title="Set up an app">
It creates a fresh app, installs the package and <code>devframe</code> from that registry, and wires the setup from the getting-started pages.
</ngmd-step>
<ngmd-step title="Build and check">
It builds the app, starts it, and checks that the hub answers <code>/__devframes/__connection.json</code> and serves the panel.
</ngmd-step>
</ngmd-workflow>

| Scenario | App |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `angular-cli` (the default) | `ng new --ssr` on the newest Angular in the peer range, with the [Express hub](../getting-started/express.md) and a development build |
| `analog` | `create-analog`, with the [Vite plugin](../getting-started/vite.md), `vite build`, and the check on the dev server |

```bash
pnpm verify:publish
pnpm verify:publish --scenario=analog
```

It uses ports 4873 (the registry) and 4874 (the app). Set `VERIFY_PORT` to move both. It needs network access to npmjs, and a run takes a few minutes. At the end it prints the versions it resolved. On a failure it leaves the app and the logs in a temporary folder and prints where.

### The weekly check

The package declares `@angular/core >=20` and `vite >=5` as peer ranges, and this repository tests on the versions it pins. The **Latest versions** workflow (`.github/workflows/latest.yml`) runs both scenarios every Monday at 06:00 UTC, and by hand from the Actions tab, on the newest versions those ranges allow. The job summary lists the versions each scenario resolved. A scheduled failure opens an issue titled `ci: the <scenario> setup fails on the latest versions`, or comments on the one already open. Close it once the fix is in.

## Publish by hand

Use this only if the workflow can't run.

### 1. Bump the version

Update `version` in `packages/devtools/package.json`. In the same commit, add a section for the version to `packages/devtools/CHANGELOG.md`. The changelog follows [Keep a Changelog](https://keepachangelog.com), with entries grouped as Upgrade notes, Security fixes, Features and Documentation. Use a message like `chore(release): 0.0.7`.

### 2. Check the build

Run the checks from [Development setup](./development.md), then build the package without publishing:
Run the checks from [Development setup](./development.md), then build and check the package without publishing:

```bash
pnpm devtools:build-pkg
pnpm verify:publish
```

It builds the package first. See [Check the package](#check-the-package).

### 3. Refresh the extension UI

If `app/` changed since the last release, run `pnpm extension:build` and commit `extension/ui` before you publish. CI fails when the committed copy is stale.
Expand All @@ -67,7 +142,7 @@ If `app/` changed since the last release, run `pnpm extension:build` and commit
pnpm devtools:publish
```

This runs `pnpm --filter @pangular-inspector/devtools publish --access public`. The `prepack` build bundles the library and the UI.
This runs `pnpm --filter @pangular-inspector/devtools publish --access public`. The `prepack` build bundles the library and the UI. It publishes without provenance, and needs an npm login with publish rights.

<ngmd-alert severity="important">
<code>pnpm publish</code> checks git before it publishes. Run it from a clean working tree on <code>main</code>.
Expand Down
Loading