diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fa1e11..f4e7ae9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -28,6 +30,8 @@ jobs: - uses: ./.github/actions/setup - uses: nrwl/nx-set-shas@v4 + with: + workflow-id: ci.yml - name: Check formatting run: pnpm format:check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a872d57 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,153 @@ +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 }} + 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('./packages/ng-devtools/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 lines change. `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\"/" packages/ng-devtools/package.json extension/manifest.json + test "$(node -p "require('./packages/ng-devtools/package.json').version")" = "$version" + test "$(node -p "require('./extension/manifest.json').version")" = "$version" + grep -rlE 'ng-devtools v[0-9]+\.[0-9]+\.[0-9]+' apps/docs/src/content | xargs -r sed -i -E "s/(ng-devtools v)[0-9]+\.[0-9]+\.[0-9]+/\1$version/" + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Check the changelog + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + mkdir -p .release + file=packages/ng-devtools/CHANGELOG.md + # This version's section, from its `## x.y.z` heading (a date and 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/ng-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 "@santoshyadavdev/ng-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="ng-devtools@$VERSION" + if git ls-remote --exit-code --tags origin "refs/tags/$tag" > /dev/null; then + echo "tag $tag already exists" + exit 0 + fi + files=(packages/ng-devtools/package.json extension/manifest.json apps/docs/src/content) + if ! git diff --quiet -- "${files[@]}"; then + git commit -m "chore(release): ng-devtools $VERSION" -- "${files[@]}" + 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 origin "refs/tags/$tag" + + - name: GitHub release + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.version.outputs.version }} + run: | + tag="ng-devtools@$VERSION" + if gh release view "$tag" > /dev/null 2>&1; then + echo "release $tag already exists" + else + gh release create "$tag" --title "ng-devtools $VERSION" --notes-file .release/notes.md + fi diff --git a/apps/docs/src/content/contributing/chrome-extension.md b/apps/docs/src/content/contributing/chrome-extension.md index 4a92380..3c1627c 100644 --- a/apps/docs/src/content/contributing/chrome-extension.md +++ b/apps/docs/src/content/contributing/chrome-extension.md @@ -78,7 +78,7 @@ This runs `extension:build`, then writes `dist/ng-devtools-extension.zip`. The z ### Upload -1. Bump `version` in `extension/manifest.json`. +1. Check that `version` in `extension/manifest.json` is the version to ship. The **Release** workflow sets it to the npm package version; see [Release the Chrome extension](./publishing.md#release-the-chrome-extension). 2. Go to the Chrome Developer Dashboard. 3. Click **New item** (or open the existing item) and upload the zip. 4. Fill in the listing details and submit for review. diff --git a/apps/docs/src/content/contributing/publishing.md b/apps/docs/src/content/contributing/publishing.md index 9f043d1..58a3e9e 100644 --- a/apps/docs/src/content/contributing/publishing.md +++ b/apps/docs/src/content/contributing/publishing.md @@ -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. --- - One npm package and one Chrome extension, each with its own version. Bump, build, publish. + One npm package and one Chrome extension, on the same version. The package releases from a workflow; the extension ships by hand. # Publishing @@ -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 + + + + On npmjs.com, open the settings of @santoshyadavdev/ng-devtools, and under Trusted publishing choose GitHub Actions with owner santoshyadavdev, repository angular-devtools and workflow release.yml. Leave the environment empty. + + + The workflow pushes the version commit and the tag with the default GITHUB_TOKEN. If a branch protection rule or ruleset guards main, allow GitHub Actions to bypass it. + + + +### Each release + +1. In a pull request, add a section for the version to `packages/ng-devtools/CHANGELOG.md`, headed with the version alone, like `## 0.0.7`. The changelog follows [Keep a Changelog](https://keepachangelog.com), with entries grouped as Upgrade notes, Security fixes, Features, Fixes 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.7`), 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/ng-devtools/package.json` and `extension/manifest.json`, and the `ng-devtools v` banner in the docs samples. | +| Check the changelog | Fails unless `CHANGELOG.md` has a `## ` 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): ng-devtools ` to `main` and tags it `ng-devtools@`. | +| 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. + + + + pnpm pack builds the package and checks that the tarball holds every exported file and the UI. + + + It starts Verdaccio with pnpm dlx and publishes the tarball there. The registry serves @santoshyadavdev/* only from what it was given, and proxies everything else to npmjs. + + + It creates a fresh app, installs the package and devframe from that registry, and wires the setup from the getting-started pages. + + + It builds the app, starts it, and checks that the hub answers /__devframes/__connection.json and serves the panel. + + + +| 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 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/ng-devtools/package.json`. In the same commit, add a section for the version to `packages/ng-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): ng-devtools 0.0.5`. +Update `version` in `packages/ng-devtools/package.json` and `extension/manifest.json`, and the `ng-devtools v` banner in `getting-started/cli.md` and `getting-started/angular-native.md`. In the same commit, add a section for the version to `packages/ng-devtools/CHANGELOG.md`. Use a message like `chore(release): ng-devtools 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. @@ -67,7 +142,7 @@ If `app/` changed since the last release, run `pnpm extension:build` and commit pnpm devtools:publish ``` -This runs `pnpm --filter @santoshyadavdev/ng-devtools publish --access public`. The `prepack` build bundles the library and the UI. +This runs `pnpm --filter @santoshyadavdev/ng-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. pnpm publish checks git before it publishes. Run it from a clean working tree on main. @@ -75,12 +150,10 @@ This runs `pnpm --filter @santoshyadavdev/ng-devtools publish --access public`. ## Release the Chrome extension -The extension has its own version, in `extension/manifest.json`. It does not follow the npm package version. +The extension's version, in `extension/manifest.json`, follows the npm package. The **Release** workflow sets it, but doesn't upload the extension. -1. Bump `version` in `extension/manifest.json`. -2. Run `pnpm extension:zip`. It rebuilds `extension/ui` first. -3. Commit `extension/ui` and the manifest. -4. Upload `dist/ng-devtools-extension.zip`. +1. After a release, run `pnpm extension:zip` on `main`. It rebuilds `extension/ui` first. +2. Upload `dist/ng-devtools-extension.zip`. See [Build the extension](./chrome-extension.md) for the upload steps.