Skip to content

Commit 6fa2be5

Browse files
Merge branch 'main' into b33-quickstart-npm-crash
2 parents 7028bf8 + eee3486 commit 6fa2be5

5 files changed

Lines changed: 144 additions & 7 deletions

File tree

‎.github/workflows/test.yml‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ jobs:
163163
name: Publish (NPM)
164164
needs: ['build', 'test', 'browser']
165165
if: ${{ github.ref == 'refs/heads/main' || github.event_name == 'release' }}
166+
# One canary publish at a time, so the check below reads the canary the previous one published.
167+
concurrency:
168+
group: ${{ github.event_name == 'release' && github.run_id || 'canary-publish' }}
169+
cancel-in-progress: false
170+
queue: max
166171
steps:
167172
- name: Setup node
168173
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -175,7 +180,37 @@ jobs:
175180
with:
176181
name: angularfire-${{ github.run_id }}
177182
path: dist
183+
- name: Skip a canary that is not newer than npm's
184+
id: canary_check
185+
run: |
186+
VERSION=$(node -p "require('./dist/packages-dist/package.json').version")
187+
if [[ $VERSION == *-canary.* ]]; then
188+
# The dist-tags endpoint is not CDN-cached, unlike the package data `npm view` reads.
189+
DIST_TAGS=$(curl -fsS --retry 3 --max-time 30 https://registry.npmjs.org/-/package/@angular/fire/dist-tags)
190+
NPM_CANARY=$(node -p "JSON.parse(process.argv[1]).canary" "$DIST_TAGS")
191+
# Order by position on main, not by version, which can be higher for an older commit.
192+
git clone --quiet --bare --filter=tree:0 "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" history.git
193+
HOW_TO_FIX="Every canary publish fails until the canary dist-tag points at a build of a commit on main. Publish rights are required to fix it with: npm dist-tag add @angular/fire@<version> canary"
194+
if ! NPM_CANARY_COMMIT=$(git -C history.git rev-parse --verify --quiet "${NPM_CANARY##*[.-]}^{commit}"); then
195+
echo "::error::Could not match the canary on npm, $NPM_CANARY, to a single commit in this repository. $HOW_TO_FIX"
196+
exit 1
197+
fi
198+
# `npm publish` always moves a dist-tag, so a canary that is not newer must not publish at all.
199+
if [[ $NPM_CANARY_COMMIT == "$GITHUB_SHA" ]]; then
200+
if [[ $VERSION == "$NPM_CANARY" ]]; then
201+
echo "::notice::Not publishing $VERSION, because it is already the canary on npm."
202+
echo "skip=true" >> "$GITHUB_OUTPUT"
203+
fi
204+
elif git -C history.git merge-base --is-ancestor "$GITHUB_SHA" "$NPM_CANARY_COMMIT"; then
205+
echo "::warning::Not publishing $VERSION, because the canary on npm, $NPM_CANARY, is from a later commit on main."
206+
echo "skip=true" >> "$GITHUB_OUTPUT"
207+
elif ! git -C history.git merge-base --is-ancestor "$NPM_CANARY_COMMIT" "$GITHUB_SHA"; then
208+
echo "::error::Not publishing $VERSION, because its commit and the commit of the canary on npm, $NPM_CANARY, are not on the same line of history. One of them is not on main. $HOW_TO_FIX"
209+
exit 1
210+
fi
211+
fi
178212
- name: Publish
213+
if: steps.canary_check.outputs.skip != 'true'
179214
run: |
180215
cd ./dist/packages-dist
181216
chmod +x publish.sh

‎tools/build.sh‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,12 @@ if [[ $GITHUB_REF =~ $TAG_TEST ]]; then
99
NPM_TAG=next
1010
fi;
1111
else
12-
FULL_VERSION=$(node -e "console.log(require('./package.json').version)")
13-
# Name the canary after the release itself, never after a prerelease of it. A canary built on
14-
# `21.0.0-rc.1` sorts above it, so the caret range `ng add` writes into a user's package.json
15-
# resolves to the canary rather than to the release candidate they asked for.
16-
BASE_VERSION=${FULL_VERSION%%-*}
17-
if [[ $BASE_VERSION != "$FULL_VERSION" ]]; then
18-
echo "package.json version is $FULL_VERSION. Naming this canary after $BASE_VERSION instead, so it does not outrank $FULL_VERSION on npm. Prereleases are published from their own git tag, so this field is meant to hold a plain release number." >&2
12+
PACKAGE_VERSION=$(node -e "console.log(require('./package.json').version)")
13+
if ! PUBLISHED_VERSIONS=$(npm view @angular/fire versions --json); then
14+
echo "Could not read the published @angular/fire versions from npm, so the canary has no version to build on." >&2
15+
exit 1
1916
fi
17+
BASE_VERSION=$(node ./tools/canary-version.js "$PACKAGE_VERSION" "$PUBLISHED_VERSIONS") || exit 1
2018
# `sha-` stops npm dropping an all-digit sha's leading zero.
2119
CANARY_ID=$(TZ=UTC git show -s --date=format-local:%Y%m%d%H%M%S --format=%cd.sha-%h $GITHUB_SHA)
2220
OVERRIDE_VERSION=$BASE_VERSION-canary.$CANARY_ID

‎tools/canary-version.jasmine.ts‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { maxSatisfying as semverMaxSatisfying } from 'semver';
2+
import { canaryBaseVersion } from './canary-version.js';
3+
import 'jasmine';
4+
5+
/* What's on npm today: a placeholder, v20 releases, rc.1, canaries named after the rc series
6+
* (deprecated), and canaries named after 21.0.0. */
7+
const publishedToday = [
8+
'0.0.0',
9+
'20.0.0',
10+
'20.1.0',
11+
'21.0.0-rc.0',
12+
'21.0.0-rc.0-canary.fb6796b',
13+
'21.0.0-rc.1',
14+
'21.0.0-rc.1-canary.95b3de1',
15+
'21.0.0-canary.a2662fe',
16+
'21.0.0-canary.20260930011755.sha-59d44e7',
17+
];
18+
19+
const canaryNamedAfter = (base: string) => `${base}-canary.20261001000000.sha-abcdef0`;
20+
21+
describe('canaryBaseVersion', () => {
22+
23+
it('names the canary one patch above the release candidate', () => {
24+
expect(canaryBaseVersion('21.0.0', publishedToday)).toBe('21.0.1');
25+
});
26+
27+
it('names the canary one patch above the release once it ships', () => {
28+
expect(canaryBaseVersion('21.0.0', [...publishedToday, '21.0.0'])).toBe('21.0.1');
29+
expect(canaryBaseVersion('21.0.0', [...publishedToday, '21.0.0', '21.0.1'])).toBe('21.0.2');
30+
});
31+
32+
it('names the canary above a minor or patch release candidate', () => {
33+
expect(canaryBaseVersion('21.0.0', [...publishedToday, '21.0.0', '21.1.0-rc.0'])).toBe('21.1.1');
34+
expect(canaryBaseVersion('21.0.0', [...publishedToday, '21.0.0', '21.0.1-rc.0'])).toBe('21.0.2');
35+
});
36+
37+
it('does not count earlier canaries', () => {
38+
const published = [...publishedToday, canaryNamedAfter('21.0.1'), canaryNamedAfter('21.0.1')];
39+
expect(canaryBaseVersion('21.0.0', published)).toBe('21.0.1');
40+
});
41+
42+
it('ignores releases from other majors', () => {
43+
expect(canaryBaseVersion('21.0.0', [...publishedToday, '21.0.0', '20.0.4'])).toBe('21.0.1');
44+
expect(canaryBaseVersion('21.0.0', [...publishedToday, '21.0.0', '22.0.0-rc.0'])).toBe('21.0.1');
45+
});
46+
47+
it('uses the package.json version when nothing in its major is published yet', () => {
48+
expect(canaryBaseVersion('22.0.0-rc.0', publishedToday)).toBe('22.0.0');
49+
expect(canaryBaseVersion('22.0.0-rc.0', [...publishedToday, '22.0.0-rc.0'])).toBe('22.0.1');
50+
});
51+
52+
it('throws when the package.json version has no version number in it', () => {
53+
expect(() => canaryBaseVersion('abc', publishedToday)).toThrowError(TypeError);
54+
});
55+
56+
it('gives a canary that its own caret range selects, and that release ranges never select', () => {
57+
const states = [
58+
publishedToday,
59+
[...publishedToday, '21.0.0'],
60+
[...publishedToday, '21.0.0', '21.0.1'],
61+
[...publishedToday, '21.0.0', '21.1.0-rc.0'],
62+
[...publishedToday, '21.0.0', '21.0.1-rc.0'],
63+
];
64+
for (const published of states) {
65+
const canary = canaryNamedAfter(canaryBaseVersion('21.0.0', published));
66+
const versions = [...published, canary];
67+
expect(semverMaxSatisfying(versions, `^${canary}`)).toBe(canary);
68+
for (const range of ['^21.0.0', '~21.0.0', '^21.0.0-rc.1', '^20.0.0']) {
69+
expect(semverMaxSatisfying(versions, range)).not.toBe(canary);
70+
}
71+
}
72+
});
73+
74+
});

‎tools/canary-version.js‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const semver = require('semver');
2+
3+
/**
4+
* Picks the version a canary build is named after, as in `<base>-canary.<time>.sha-<sha>`.
5+
*
6+
* `ng add @angular/fire@canary` saves `^<canary>`, and npm installs the highest version in that
7+
* range, so the canary must rank above every release and release candidate already published
8+
* in its major.
9+
*
10+
* @param {string} packageVersion The `version` field of the repository's package.json.
11+
* @param {string[]} publishedVersions Every @angular/fire version on npm.
12+
* @returns {string} One patch above the highest published version of the same major, or the
13+
* package.json version without its prerelease part if that is higher.
14+
*/
15+
function canaryBaseVersion(packageVersion, publishedVersions) {
16+
const packageBase = semver.coerce(packageVersion);
17+
// Published canaries don't count, or every canary would be named one patch above the one before it.
18+
const releases = publishedVersions.filter(
19+
version => !version.includes('canary') && semver.major(version) === packageBase.major,
20+
);
21+
const nextPatch = releases.length ? semver.coerce(semver.rsort(releases)[0]).inc('patch') : packageBase;
22+
return semver.gt(packageBase, nextPatch) ? packageBase.version : nextPatch.version;
23+
}
24+
25+
module.exports = { canaryBaseVersion };
26+
27+
if (require.main === module) {
28+
console.log(canaryBaseVersion(process.argv[2], JSON.parse(process.argv[3])));
29+
}

‎tsconfig.jasmine.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
},
1515
"include": [
1616
"tools/jasmine.ts",
17+
"tools/**/*.jasmine.ts",
1718
"src/**/*.jasmine.ts",
1819
// Not sure what is wrong here, but since upgrading karma it's fallen apart
1920
// "src/**/*.spec.ts",

0 commit comments

Comments
 (0)