Skip to content

Commit 93cf0f1

Browse files
authored
Merge pull request #278 from knockout/bmh/sc-automate-github-release
release: automate GitHub releases
2 parents 01e2272 + e9a09ed commit 93cf0f1

File tree

7 files changed

+221
-5
lines changed

7 files changed

+221
-5
lines changed

.changeset/config.json

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,37 @@
22
"$schema": "https://unpkg.com/@changesets/config@3.1.3/schema.json",
33
"changelog": "@changesets/cli/changelog",
44
"commit": false,
5-
"fixed": [],
5+
"fixed": [
6+
[
7+
"@tko/bind",
8+
"@tko/binding.component",
9+
"@tko/binding.core",
10+
"@tko/binding.foreach",
11+
"@tko/binding.if",
12+
"@tko/binding.template",
13+
"@tko/build.knockout",
14+
"@tko/build.reference",
15+
"@tko/builder",
16+
"@tko/computed",
17+
"@tko/filter.punches",
18+
"@tko/lifecycle",
19+
"@tko/observable",
20+
"@tko/provider",
21+
"@tko/provider.attr",
22+
"@tko/provider.bindingstring",
23+
"@tko/provider.component",
24+
"@tko/provider.databind",
25+
"@tko/provider.multi",
26+
"@tko/provider.mustache",
27+
"@tko/provider.native",
28+
"@tko/provider.virtual",
29+
"@tko/utils",
30+
"@tko/utils.component",
31+
"@tko/utils.functionrewrite",
32+
"@tko/utils.jsx",
33+
"@tko/utils.parser"
34+
]
35+
],
636
"linked": [],
737
"access": "public",
838
"baseBranch": "main",
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: Backfill GitHub Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
target_sha:
7+
description: Published main commit SHA to tag and release
8+
required: true
9+
type: string
10+
11+
jobs:
12+
backfill-github-release:
13+
runs-on: ubuntu-latest
14+
15+
permissions:
16+
contents: write
17+
18+
steps:
19+
- name: Checkout published commit
20+
# actions/checkout v6.0.2
21+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
22+
with:
23+
ref: ${{ inputs.target_sha }}
24+
fetch-depth: 0
25+
persist-credentials: false
26+
27+
- name: Validate target commit and determine release version
28+
id: version
29+
env:
30+
TARGET_SHA: ${{ inputs.target_sha }}
31+
run: |
32+
git fetch origin main --depth=1
33+
34+
if ! git merge-base --is-ancestor "$TARGET_SHA" "origin/main"; then
35+
echo "Target SHA $TARGET_SHA is not reachable from origin/main." >&2
36+
exit 1
37+
fi
38+
39+
version="$(node tools/release-version.cjs)"
40+
echo "version=$version" >> "$GITHUB_OUTPUT"
41+
42+
- name: Verify published npm version exists
43+
env:
44+
VERSION: ${{ steps.version.outputs.version }}
45+
run: |
46+
npm view "@tko/build.reference@$VERSION" version --registry=https://registry.npmjs.org >/dev/null
47+
48+
- name: Backfill GitHub release if missing
49+
env:
50+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51+
VERSION: ${{ steps.version.outputs.version }}
52+
TARGET_SHA: ${{ inputs.target_sha }}
53+
run: |
54+
tag="v${VERSION}"
55+
56+
if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
57+
object_type="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" --jq '.object.type')"
58+
object_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" --jq '.object.sha')"
59+
60+
if [ "$object_type" = "tag" ]; then
61+
existing_target="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha" --jq '.object.sha')"
62+
else
63+
existing_target="$object_sha"
64+
fi
65+
66+
if [ "$existing_target" = "$TARGET_SHA" ]; then
67+
echo "GitHub release $tag already exists on $TARGET_SHA; nothing to do."
68+
exit 0
69+
fi
70+
71+
echo "GitHub release $tag already exists on $existing_target, expected $TARGET_SHA." >&2
72+
exit 1
73+
fi
74+
75+
prerelease_flag=""
76+
case "$VERSION" in
77+
*-alpha*|*-beta*|*-rc*)
78+
prerelease_flag="--prerelease"
79+
;;
80+
esac
81+
82+
gh release create "$tag" \
83+
--repo "$GITHUB_REPOSITORY" \
84+
--target "$TARGET_SHA" \
85+
--title "TKO ${VERSION}" \
86+
--generate-notes \
87+
$prerelease_flag

.github/workflows/release.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ jobs:
3232
- name: Checkout code
3333
# actions/checkout v6.0.2
3434
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
35+
with:
36+
persist-credentials: false
3537

3638
- name: Setup Node.js
3739
# actions/setup-node v6.3.0
@@ -66,6 +68,9 @@ jobs:
6668
if: needs.prepare-release.outputs.has_changesets == 'false'
6769
runs-on: ubuntu-latest
6870

71+
outputs:
72+
release_version: ${{ steps.version.outputs.version }}
73+
6974
permissions:
7075
contents: read
7176
id-token: write
@@ -74,6 +79,8 @@ jobs:
7479
- name: Checkout code
7580
# actions/checkout v6.0.2
7681
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
82+
with:
83+
persist-credentials: false
7784

7885
- name: Setup Node.js
7986
# actions/setup-node v6.3.0
@@ -88,6 +95,12 @@ jobs:
8895
- name: Install dependencies
8996
run: npm ci
9097

98+
- name: Determine release version
99+
id: version
100+
run: |
101+
version="$(node tools/release-version.cjs)"
102+
echo "version=$version" >> "$GITHUB_OUTPUT"
103+
91104
- name: Build all packages
92105
run: make
93106

@@ -98,3 +111,54 @@ jobs:
98111
# npm generates provenance attestations automatically during trusted
99112
# publishing from GitHub Actions.
100113
run: npx changeset publish
114+
115+
github-release:
116+
name: Create GitHub Release
117+
needs: [prepare-release, publish]
118+
if: needs.prepare-release.outputs.has_changesets == 'false' && needs.publish.result == 'success'
119+
runs-on: ubuntu-latest
120+
121+
permissions:
122+
contents: write
123+
124+
steps:
125+
- name: Create GitHub release
126+
env:
127+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
128+
VERSION: ${{ needs.publish.outputs.release_version }}
129+
TARGET_SHA: ${{ github.sha }}
130+
run: |
131+
tag="v${VERSION}"
132+
133+
if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
134+
object_type="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" --jq '.object.type')"
135+
object_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" --jq '.object.sha')"
136+
137+
if [ "$object_type" = "tag" ]; then
138+
existing_target="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha" --jq '.object.sha')"
139+
else
140+
existing_target="$object_sha"
141+
fi
142+
143+
if [ "$existing_target" = "$TARGET_SHA" ]; then
144+
echo "GitHub release $tag already exists on $TARGET_SHA; skipping."
145+
exit 0
146+
fi
147+
148+
echo "GitHub release $tag already exists on $existing_target, expected $TARGET_SHA." >&2
149+
exit 1
150+
fi
151+
152+
prerelease_flag=""
153+
case "$VERSION" in
154+
*-alpha*|*-beta*|*-rc*)
155+
prerelease_flag="--prerelease"
156+
;;
157+
esac
158+
159+
gh release create "$tag" \
160+
--repo "$GITHUB_REPOSITORY" \
161+
--target "$TARGET_SHA" \
162+
--title "TKO ${VERSION}" \
163+
--generate-notes \
164+
$prerelease_flag

AGENTS.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ GitHub Actions workflows (`.github/workflows/`):
106106
| `test-headless.yml` | PRs | Matrix test (Chrome, Firefox, jQuery) |
107107
| `lint-and-typecheck.yml` | PRs | Prettier + ESLint + tsc (combined) |
108108
| `publish-check.yml` | PRs | Verify packages are publishable |
109-
| `release.yml` | Push to main | Changeset version PRs + npm publish |
109+
| `release.yml` | Push to main | Changeset version PRs + npm publish + GitHub release creation |
110+
| `github-release.yml` | Manual fallback | Backfill a GitHub release/tag for a published `main` commit if automatic release creation needs a retry |
110111
| `deploy-docs.yml` | Push to main | Deploy tko.io to GitHub Pages |
111112
| `codeql-analysis.yml` | Weekly + main push | Security scanning |
112113

@@ -116,6 +117,10 @@ All PR checks must pass before merge.
116117

117118
Releases are managed with [Changesets](https://github.com/changesets/changesets).
118119

120+
TKO uses a repo-wide fixed release line for all public `@tko/*` packages. A
121+
release that bumps any public package bumps the full public package set to the
122+
same version.
123+
119124
**For contributors** — when your PR changes package behavior:
120125
```bash
121126
npx changeset add # Select affected packages, bump type, describe change
@@ -127,6 +132,8 @@ This creates a changeset file in `.changeset/` that gets committed with your PR.
127132
2. If unreleased changesets exist, the action opens a "Version Packages" PR
128133
3. Review the PR (it bumps versions and updates changelogs)
129134
4. Merge it to publish to npm via GitHub Actions OIDC trusted publishing
135+
5. The same release workflow creates the matching GitHub Release and tag after a successful publish
136+
6. If GitHub release creation ever needs a retry after publish, run `github-release.yml` manually with the merged commit SHA
130137

131138
Avoid manual workstation publishes. If release CI is unavailable, fix the
132139
workflow or npm trusted publisher configuration rather than bypassing it with a

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ It's available as `@tko/build.knockout`, and over CDN:
5555
| $ `make test` | Run all tests with electron. See below. |
5656
| $ `make test-headless` | Run all tests with chromium. See below. |
5757
| $ `npx changeset add` | Add a changeset for package behavior changes in your PR |
58-
| Release workflow | On merge to `main`, CI opens or updates a version PR; when that version PR is merged and there are no remaining changesets, CI publishes from GitHub Actions via npm trusted publishing |
58+
| Release workflow | On merge to `main`, CI opens or updates a version PR; when that version PR is merged and there are no remaining changesets, the same workflow publishes from GitHub Actions via npm trusted publishing and creates the matching GitHub release/tag |
59+
| GitHub release workflow | Manual fallback to backfill the GitHub release/tag for a published `main` commit if automatic release creation needs a retry |
5960
| $ `make test-coverage` | Run all tests and create a code coverage report |
6061

6162
Checkout the `Makefile` for more commands that can be executed with `make {command}`.

plans/build-and-release-certainty.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,20 @@ New workflow `.github/workflows/publish-check.yml` that runs on PRs:
6262
Installed `@changesets/cli`. Configuration in `.changeset/config.json`.
6363
Contributors add a changeset file with their PR: `npx changeset add`.
6464
On merge to main, the release workflow opens a "Version Packages" PR
65-
that bumps versions and updates changelogs. Merging that PR publishes
66-
to npm.
65+
that bumps versions and updates changelogs. TKO now uses a repo-wide
66+
fixed version group for all public `@tko/*` packages, so public releases
67+
move together on one version line. Merging that PR publishes to npm.
6768

6869
### 2.2 Add a release workflow ✅ DONE
6970

7071
`.github/workflows/release.yml` — triggered on push to main:
7172
- Builds all packages and runs tests
7273
- If unreleased changesets exist, opens/updates a version PR
7374
- If version PR is merged, publishes to npm
75+
- After a successful publish, creates the matching GitHub Release and tag in the same workflow
7476
- Uses npm trusted publishing via GitHub Actions OIDC
7577
- Requires trusted publisher configuration for the public `@tko/*` packages on npm
78+
- Includes a manual `github-release.yml` workflow to backfill a missing release/tag for a published `main` commit if GitHub release creation ever needs a retry after publish
7679

7780
---
7881

tools/release-version.cjs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const fs = require('fs')
2+
const path = require('path')
3+
4+
const roots = ['builds', 'packages']
5+
const versions = new Set()
6+
7+
for (const root of roots) {
8+
for (const name of fs.readdirSync(root)) {
9+
const packageJson = path.join(root, name, 'package.json')
10+
if (!fs.existsSync(packageJson)) continue
11+
12+
const pkg = JSON.parse(fs.readFileSync(packageJson, 'utf8'))
13+
if (pkg.private) continue
14+
15+
versions.add(pkg.version)
16+
}
17+
}
18+
19+
if (versions.size !== 1) {
20+
console.error(`Expected one public package version, found: ${[...versions].sort().join(', ')}`)
21+
process.exit(1)
22+
}
23+
24+
process.stdout.write([...versions][0])

0 commit comments

Comments
 (0)