diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d9984d..d72cd4f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -155,6 +155,19 @@ jobs: git tag -a "v$version" -m "Release v$version" -m "$published" git push origin "v$version" fi + + # A package that moved on its own (angular 1.11.1 next to core 1.11.0) has no + # v tag to stand for it: it gets one named after itself. + own=$(node scripts/own-release-packages.js) + while read -r tag pkg pv; do + [ -n "$tag" ] || continue + if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then + echo "Tag $tag already exists - leaving it alone." + else + git tag -a "$tag" -m "Release $tag" + git push origin "$tag" + fi + done <<< "$own" env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} @@ -174,26 +187,44 @@ jobs: if gh release view "v$version" >/dev/null 2>&1; then echo "Release v$version already exists - leaving it alone." - exit 0 + else + { + echo 'All five packages ship at this version with npm provenance:' + echo + for pkg in core react vue angular svelte; do + echo "- \`@dynamic-field-kit/$pkg@$(node -p "require('./packages/$pkg/package.json').version")\`" + done + echo + awk -v heading="## $version" \ + '$0 == heading { found = 1; next } /^## / { if (found) exit } found' \ + packages/core/CHANGELOG.md + } > "$RUNNER_TEMP/release-notes.md" + + gh release create "v$version" \ + --title "v$version" \ + --notes-file "$RUNNER_TEMP/release-notes.md" \ + --verify-tag \ + --latest fi - { - echo 'All five packages ship at this version with npm provenance:' - echo - for pkg in core react vue angular svelte; do - echo "- \`@dynamic-field-kit/$pkg@$(node -p "require('./packages/$pkg/package.json').version")\`" - done - echo - awk -v heading="## $version" \ + # A package that moved on its own gets a Release of its own, from its own + # CHANGELOG. It is not marked latest: v stays the release people land on. + own=$(node scripts/own-release-packages.js) + while read -r tag pkg pv; do + [ -n "$tag" ] || continue + if gh release view "$tag" >/dev/null 2>&1; then + echo "Release $tag already exists - leaving it alone." + continue + fi + awk -v heading="## $pv" \ '$0 == heading { found = 1; next } /^## / { if (found) exit } found' \ - packages/core/CHANGELOG.md - } > "$RUNNER_TEMP/release-notes.md" - - gh release create "v$version" \ - --title "v$version" \ - --notes-file "$RUNNER_TEMP/release-notes.md" \ - --verify-tag \ - --latest + "packages/$pkg/CHANGELOG.md" > "$RUNNER_TEMP/release-notes-$pkg.md" + gh release create "$tag" \ + --title "$tag" \ + --notes-file "$RUNNER_TEMP/release-notes-$pkg.md" \ + --verify-tag \ + --latest=false + done <<< "$own" env: GH_TOKEN: ${{ github.token }} @@ -202,4 +233,6 @@ jobs: run: | echo 'Dry run - nothing was committed, tagged or published.' echo 'Versions that would have shipped are listed above.' + echo 'Packages that would get a tag and a Release of their own (tag, package, version):' + node scripts/own-release-packages.js git --no-pager diff --stat diff --git a/scripts/own-release-packages.js b/scripts/own-release-packages.js new file mode 100644 index 0000000..6a4e464 --- /dev/null +++ b/scripts/own-release-packages.js @@ -0,0 +1,64 @@ +#!/usr/bin/env node +/** + * Lists the packages that need a git tag and a GitHub Release of their own. + * + * A release is tagged `v` after core: the five packages share one version + * line and core anchors it. The `linked` group still lets one package move on its + * own, as angular did in 1.11.1 while core stayed at 1.11.0. That version has no + * `v` tag to stand for it, and release.yml made neither a tag nor a Release + * for it (both were made by hand). Such a package gets + * `@dynamic-field-kit/@`, the name changesets gives a package tag. + * + * A package that is behind core at a version an older `v` already covers + * needs nothing. + * + * Prints one line per package: ` `. + */ +const { execFileSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const PACKAGES = ['react', 'vue', 'angular', 'svelte']; + +/** + * @param {Record} versions package name -> version, core included + * @param {(tag: string) => boolean} tagExists + */ +function ownReleasePackages(versions, tagExists) { + return PACKAGES.filter( + (pkg) => versions[pkg] !== versions.core && !tagExists(`v${versions[pkg]}`), + ).map((pkg) => ({ + tag: `@dynamic-field-kit/${pkg}@${versions[pkg]}`, + pkg, + version: versions[pkg], + })); +} + +function readVersions(root) { + const versions = {}; + for (const pkg of ['core', ...PACKAGES]) { + const manifest = path.join(root, 'packages', pkg, 'package.json'); + versions[pkg] = JSON.parse(fs.readFileSync(manifest, 'utf8')).version; + } + return versions; +} + +function gitTagExists(tag) { + try { + execFileSync('git', ['rev-parse', '-q', '--verify', `refs/tags/${tag}`], { + stdio: 'ignore', + }); + return true; + } catch { + return false; + } +} + +if (require.main === module) { + const own = ownReleasePackages(readVersions(process.cwd()), gitTagExists); + for (const { tag, pkg, version } of own) { + console.log(`${tag} ${pkg} ${version}`); + } +} + +module.exports = { ownReleasePackages, readVersions, PACKAGES }; diff --git a/scripts/own-release-packages.test.js b/scripts/own-release-packages.test.js new file mode 100644 index 0000000..ebec0bd --- /dev/null +++ b/scripts/own-release-packages.test.js @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest'; + +import { ownReleasePackages } from './own-release-packages.js'; + +const line = (version, overrides = {}) => ({ + core: version, + react: version, + vue: version, + angular: version, + svelte: version, + ...overrides, +}); +const tags = + (...names) => + (tag) => + names.includes(tag); + +describe('ownReleasePackages', () => { + it('lists nothing when every package is on core version', () => { + expect(ownReleasePackages(line('1.12.0'), tags())).toEqual([]); + }); + + it('lists a package that moved on its own, as angular did in 1.11.1', () => { + expect( + ownReleasePackages( + line('1.11.0', { angular: '1.11.1' }), + tags('v1.11.0'), + ), + ).toEqual([ + { + tag: '@dynamic-field-kit/angular@1.11.1', + pkg: 'angular', + version: '1.11.1', + }, + ]); + }); + + it('leaves out a package behind core whose version an older release tag covers', () => { + expect( + ownReleasePackages( + line('1.12.0', { react: '1.11.0' }), + tags('v1.11.0', 'v1.12.0'), + ), + ).toEqual([]); + }); + + it('lists every package that moved on its own', () => { + const own = ownReleasePackages( + line('1.11.0', { vue: '1.11.1', svelte: '1.11.2' }), + tags('v1.11.0'), + ); + expect(own.map(({ tag }) => tag)).toEqual([ + '@dynamic-field-kit/vue@1.11.1', + '@dynamic-field-kit/svelte@1.11.2', + ]); + }); +});