From aeb407296b535c453fa08017a82b3de0e7902f2b Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Thu, 27 May 2021 12:14:36 -0700 Subject: [PATCH] refactor(dev-infra): update to later version of @octokit/rest and remove class extenstion of Octokit (#42395) Update @octokit/rest and remove the usage of a class extension of Octokit as the class does not have a class define constructor. PR Close #42395 --- dev-infra/build-worker.js | 56 ++++------------ dev-infra/ng-dev.js | 59 +++++----------- dev-infra/pr/merge/strategies/api-merge.ts | 9 +-- .../release/publish/pull-request-state.ts | 2 +- .../release/versioning/version-branches.ts | 3 +- dev-infra/utils/git/github.ts | 41 ++++-------- dev-infra/utils/git/index.ts | 2 +- package.json | 2 +- yarn.lock | 67 +++++++++++++------ 9 files changed, 98 insertions(+), 143 deletions(-) diff --git a/dev-infra/build-worker.js b/dev-infra/build-worker.js index e392a8010d898..16f67521b0a3e 100644 --- a/dev-infra/build-worker.js +++ b/dev-infra/build-worker.js @@ -8,7 +8,7 @@ require('inquirer'); var child_process = require('child_process'); var semver = require('semver'); var graphql = require('@octokit/graphql'); -var Octokit = require('@octokit/rest'); +var rest = require('@octokit/rest'); var typedGraphqlify = require('typed-graphqlify'); var url = require('url'); @@ -68,32 +68,27 @@ var GithubGraphqlClientError = /** @class */ (function (_super) { * Additionally, provides convenience methods for actions which require multiple requests, or * would provide value from memoized style responses. **/ -var GithubClient = /** @class */ (function (_super) { - tslib.__extends(GithubClient, _super); +var GithubClient = /** @class */ (function () { /** * @param token The github authentication token for Github Rest and Graphql API requests. */ function GithubClient(token) { - var _this = - // Pass in authentication token to base Octokit class. - _super.call(this, { auth: token }) || this; - _this.token = token; - /** The current user based on checking against the Github API. */ - _this._currentUser = null; + this.token = token; /** The graphql instance with authentication set during construction. */ - _this._graphql = graphql.graphql.defaults({ headers: { authorization: "token " + _this.token } }); - _this.hook.error('request', function (error) { + this._graphql = graphql.graphql.defaults({ headers: { authorization: "token " + this.token } }); + /** The Octokit instance actually performing API requests. */ + this._octokit = new rest.Octokit({ token: this.token }); + this.pulls = this._octokit.pulls; + this.repos = this._octokit.repos; + this.issues = this._octokit.issues; + this.git = this._octokit.git; + this.paginate = this._octokit.paginate; + this.rateLimit = this._octokit.rateLimit; + this._octokit.hook.error('request', function (error) { // Wrap API errors in a known error class. This allows us to // expect Github API errors better and in a non-ambiguous way. throw new GithubApiRequestError(error.status, error.message); }); - // Note: The prototype must be set explictly as Github's Octokit class is a non-standard class - // definition which adjusts the prototype chain. - // See: - // https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work - // https://github.com/octokit/rest.js/blob/7b51cee4a22b6e52adcdca011f93efdffa5df998/lib/constructor.js - Object.setPrototypeOf(_this, GithubClient.prototype); - return _this; } /** Perform a query using Github's Graphql API. */ GithubClient.prototype.graphql = function (queryObject, params) { @@ -112,31 +107,8 @@ var GithubClient = /** @class */ (function (_super) { }); }); }; - /** Retrieve the login of the current user from Github. */ - GithubClient.prototype.getCurrentUser = function () { - return tslib.__awaiter(this, void 0, void 0, function () { - var result; - return tslib.__generator(this, function (_a) { - switch (_a.label) { - case 0: - // If the current user has already been retrieved return the current user value again. - if (this._currentUser !== null) { - return [2 /*return*/, this._currentUser]; - } - return [4 /*yield*/, this.graphql({ - viewer: { - login: typedGraphqlify.types.string, - } - })]; - case 1: - result = _a.sent(); - return [2 /*return*/, this._currentUser = result.viewer.login]; - } - }); - }); - }; return GithubClient; -}(Octokit)); +}()); /** * @license diff --git a/dev-infra/ng-dev.js b/dev-infra/ng-dev.js index 2846b3f33c47f..a2285c233b7c5 100755 --- a/dev-infra/ng-dev.js +++ b/dev-infra/ng-dev.js @@ -12,7 +12,7 @@ var path = require('path'); var child_process = require('child_process'); var semver = require('semver'); var graphql = require('@octokit/graphql'); -var Octokit = require('@octokit/rest'); +var rest = require('@octokit/rest'); var typedGraphqlify = require('typed-graphqlify'); var url = require('url'); var fetch = _interopDefault(require('node-fetch')); @@ -231,32 +231,27 @@ var GithubGraphqlClientError = /** @class */ (function (_super) { * Additionally, provides convenience methods for actions which require multiple requests, or * would provide value from memoized style responses. **/ -var GithubClient = /** @class */ (function (_super) { - tslib.__extends(GithubClient, _super); +var GithubClient = /** @class */ (function () { /** * @param token The github authentication token for Github Rest and Graphql API requests. */ function GithubClient(token) { - var _this = - // Pass in authentication token to base Octokit class. - _super.call(this, { auth: token }) || this; - _this.token = token; - /** The current user based on checking against the Github API. */ - _this._currentUser = null; + this.token = token; /** The graphql instance with authentication set during construction. */ - _this._graphql = graphql.graphql.defaults({ headers: { authorization: "token " + _this.token } }); - _this.hook.error('request', function (error) { + this._graphql = graphql.graphql.defaults({ headers: { authorization: "token " + this.token } }); + /** The Octokit instance actually performing API requests. */ + this._octokit = new rest.Octokit({ token: this.token }); + this.pulls = this._octokit.pulls; + this.repos = this._octokit.repos; + this.issues = this._octokit.issues; + this.git = this._octokit.git; + this.paginate = this._octokit.paginate; + this.rateLimit = this._octokit.rateLimit; + this._octokit.hook.error('request', function (error) { // Wrap API errors in a known error class. This allows us to // expect Github API errors better and in a non-ambiguous way. throw new GithubApiRequestError(error.status, error.message); }); - // Note: The prototype must be set explictly as Github's Octokit class is a non-standard class - // definition which adjusts the prototype chain. - // See: - // https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work - // https://github.com/octokit/rest.js/blob/7b51cee4a22b6e52adcdca011f93efdffa5df998/lib/constructor.js - Object.setPrototypeOf(_this, GithubClient.prototype); - return _this; } /** Perform a query using Github's Graphql API. */ GithubClient.prototype.graphql = function (queryObject, params) { @@ -275,31 +270,8 @@ var GithubClient = /** @class */ (function (_super) { }); }); }; - /** Retrieve the login of the current user from Github. */ - GithubClient.prototype.getCurrentUser = function () { - return tslib.__awaiter(this, void 0, void 0, function () { - var result; - return tslib.__generator(this, function (_a) { - switch (_a.label) { - case 0: - // If the current user has already been retrieved return the current user value again. - if (this._currentUser !== null) { - return [2 /*return*/, this._currentUser]; - } - return [4 /*yield*/, this.graphql({ - viewer: { - login: typedGraphqlify.types.string, - } - })]; - case 1: - result = _a.sent(); - return [2 /*return*/, this._currentUser = result.viewer.login]; - } - }); - }); - }; return GithubClient; -}(Octokit)); +}()); /** * @license @@ -882,7 +854,8 @@ const versionBranchNameRegex = /^(\d+)\.(\d+)\.x$/; function getVersionOfBranch(repo, branchName) { return tslib.__awaiter(this, void 0, void 0, function* () { const { data } = yield repo.api.repos.getContents({ owner: repo.owner, repo: repo.name, path: '/package.json', ref: branchName }); - const { version } = JSON.parse(Buffer.from(data.content, 'base64').toString()); + const content = Array.isArray(data) ? '' : data.content || ''; + const { version } = JSON.parse(Buffer.from(content, 'base64').toString()); const parsedVersion = semver.parse(version); if (parsedVersion === null) { throw Error(`Invalid version detected in following branch: ${branchName}.`); diff --git a/dev-infra/pr/merge/strategies/api-merge.ts b/dev-infra/pr/merge/strategies/api-merge.ts index 76b9798731699..e66aa8d3098e1 100644 --- a/dev-infra/pr/merge/strategies/api-merge.ts +++ b/dev-infra/pr/merge/strategies/api-merge.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {PullsListCommitsResponse, PullsMergeParams} from '@octokit/rest'; +import {Octokit} from '@octokit/rest'; import {prompt} from 'inquirer'; import {parseCommitMessage} from '../../../commit-message/parse'; @@ -74,7 +74,7 @@ export class GithubApiMergeStrategy extends MergeStrategy { return failure; } - const mergeOptions: PullsMergeParams = { + const mergeOptions: Octokit.PullsMergeParams = { pull_number: prNumber, merge_method: method, ...this.git.remoteParams, @@ -159,7 +159,8 @@ export class GithubApiMergeStrategy extends MergeStrategy { * strategy, we cannot start an interactive rebase because we merge using the Github API. * The Github API only allows modifications to PR title and body for squash merges. */ - private async _promptCommitMessageEdit(pullRequest: PullRequest, mergeOptions: PullsMergeParams) { + private async _promptCommitMessageEdit( + pullRequest: PullRequest, mergeOptions: Octokit.PullsMergeParams) { const commitMessage = await this._getDefaultSquashCommitMessage(pullRequest); const {result} = await prompt<{result: string}>({ type: 'editor', @@ -197,7 +198,7 @@ export class GithubApiMergeStrategy extends MergeStrategy { private async _getPullRequestCommitMessages({prNumber}: PullRequest) { const request = this.git.github.pulls.listCommits.endpoint.merge( {...this.git.remoteParams, pull_number: prNumber}); - const allCommits: PullsListCommitsResponse = await this.git.github.paginate(request); + const allCommits: Octokit.PullsListCommitsResponse = await this.git.github.paginate(request); return allCommits.map(({commit}) => commit.message); } diff --git a/dev-infra/release/publish/pull-request-state.ts b/dev-infra/release/publish/pull-request-state.ts index 8a01def36726b..38cb227302cfa 100644 --- a/dev-infra/release/publish/pull-request-state.ts +++ b/dev-infra/release/publish/pull-request-state.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import * as Octokit from '@octokit/rest'; +import {Octokit} from '@octokit/rest'; import {GitClient} from '../../utils/git/index'; /** Thirty seconds in milliseconds. */ diff --git a/dev-infra/release/versioning/version-branches.ts b/dev-infra/release/versioning/version-branches.ts index 5db34f82c3e01..b4db72601a5bf 100644 --- a/dev-infra/release/versioning/version-branches.ts +++ b/dev-infra/release/versioning/version-branches.ts @@ -35,7 +35,8 @@ export async function getVersionOfBranch( repo: GithubRepoWithApi, branchName: string): Promise { const {data} = await repo.api.repos.getContents( {owner: repo.owner, repo: repo.name, path: '/package.json', ref: branchName}); - const {version} = JSON.parse(Buffer.from(data.content, 'base64').toString()) as + const content = Array.isArray(data) ? '' : data.content || ''; + const {version} = JSON.parse(Buffer.from(content, 'base64').toString()) as {version: string, [key: string]: any}; const parsedVersion = semver.parse(version); if (parsedVersion === null) { diff --git a/dev-infra/utils/git/github.ts b/dev-infra/utils/git/github.ts index 2e243800580e3..ad97620c089da 100644 --- a/dev-infra/utils/git/github.ts +++ b/dev-infra/utils/git/github.ts @@ -7,9 +7,9 @@ */ import {graphql} from '@octokit/graphql'; -import * as Octokit from '@octokit/rest'; +import {Octokit} from '@octokit/rest'; import {RequestParameters} from '@octokit/types'; -import {query, types} from 'typed-graphqlify'; +import {query} from 'typed-graphqlify'; /** * An object representation of a Graphql Query to be used as a response type and @@ -41,31 +41,21 @@ export class GithubGraphqlClientError extends Error {} * Additionally, provides convenience methods for actions which require multiple requests, or * would provide value from memoized style responses. **/ -export class GithubClient extends Octokit { - /** The current user based on checking against the Github API. */ - private _currentUser: string|null = null; +export class GithubClient { /** The graphql instance with authentication set during construction. */ private _graphql = graphql.defaults({headers: {authorization: `token ${this.token}`}}); + /** The Octokit instance actually performing API requests. */ + private _octokit = new Octokit({token: this.token}); /** * @param token The github authentication token for Github Rest and Graphql API requests. */ constructor(private token?: string) { - // Pass in authentication token to base Octokit class. - super({auth: token}); - - this.hook.error('request', error => { + this._octokit.hook.error('request', error => { // Wrap API errors in a known error class. This allows us to // expect Github API errors better and in a non-ambiguous way. throw new GithubApiRequestError(error.status, error.message); }); - - // Note: The prototype must be set explictly as Github's Octokit class is a non-standard class - // definition which adjusts the prototype chain. - // See: - // https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work - // https://github.com/octokit/rest.js/blob/7b51cee4a22b6e52adcdca011f93efdffa5df998/lib/constructor.js - Object.setPrototypeOf(this, GithubClient.prototype); } /** Perform a query using Github's Graphql API. */ @@ -78,17 +68,10 @@ export class GithubClient extends Octokit { return (await this._graphql(query(queryObject).toString(), params)) as T; } - /** Retrieve the login of the current user from Github. */ - async getCurrentUser() { - // If the current user has already been retrieved return the current user value again. - if (this._currentUser !== null) { - return this._currentUser; - } - const result = await this.graphql({ - viewer: { - login: types.string, - } - }); - return this._currentUser = result.viewer.login; - } + pulls = this._octokit.pulls; + repos = this._octokit.repos; + issues = this._octokit.issues; + git = this._octokit.git; + paginate = this._octokit.paginate; + rateLimit = this._octokit.rateLimit; } diff --git a/dev-infra/utils/git/index.ts b/dev-infra/utils/git/index.ts index 6dfb5c1cc695e..9e7f9b8f83ebb 100644 --- a/dev-infra/utils/git/index.ts +++ b/dev-infra/utils/git/index.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import * as Octokit from '@octokit/rest'; +import {Octokit} from '@octokit/rest'; import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process'; import {Options as SemVerOptions, parse, SemVer} from 'semver'; diff --git a/package.json b/package.json index 577b19708fa4f..511a2a8f04090 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@bazel/terser": "3.5.0", "@bazel/typescript": "3.5.0", "@microsoft/api-extractor": "7.7.11", - "@octokit/rest": "16.28.7", + "@octokit/rest": "16.43.2", "@octokit/types": "^6.0.0", "@schematics/angular": "12.0.1", "@types/angular": "^1.6.47", diff --git a/yarn.lock b/yarn.lock index dffc956a2f3a5..f2a70df66c3e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1277,6 +1277,13 @@ node-gyp "^7.1.0" read-package-json-fast "^2.0.1" +"@octokit/auth-token@^2.4.0": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3" + integrity sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA== + dependencies: + "@octokit/types" "^6.0.3" + "@octokit/endpoint@^6.0.1": version "6.0.11" resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.11.tgz#082adc2aebca6dcefa1fb383f5efb3ed081949d1" @@ -1300,6 +1307,26 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-7.0.0.tgz#0f6992db9854af15eca77d71ab0ec7fad2f20411" integrity sha512-gV/8DJhAL/04zjTI95a7FhQwS6jlEE0W/7xeYAzuArD0KVAVWDLP2f3vi98hs3HLTczxXdRK/mF0tRoQPpolEw== +"@octokit/plugin-paginate-rest@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz#004170acf8c2be535aba26727867d692f7b488fc" + integrity sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q== + dependencies: + "@octokit/types" "^2.0.1" + +"@octokit/plugin-request-log@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.3.tgz#70a62be213e1edc04bb8897ee48c311482f9700d" + integrity sha512-4RFU4li238jMJAzLgAwkBAw+4Loile5haQMQr+uhFq27BmyJXcXSKvoQKqh0agsZEiUlW6iSv3FAgvmGkur7OQ== + +"@octokit/plugin-rest-endpoint-methods@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz#3288ecf5481f68c494dd0602fc15407a59faf61e" + integrity sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ== + dependencies: + "@octokit/types" "^2.0.1" + deprecation "^2.3.1" + "@octokit/request-error@^1.0.2": version "1.2.1" resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801" @@ -1318,7 +1345,7 @@ deprecation "^2.0.0" once "^1.4.0" -"@octokit/request@^5.0.0", "@octokit/request@^5.3.0": +"@octokit/request@^5.2.0", "@octokit/request@^5.3.0": version "5.4.15" resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.15.tgz#829da413dc7dd3aa5e2cdbb1c7d0ebe1f146a128" integrity sha512-6UnZfZzLwNhdLRreOtTkT9n57ZwulCve8q3IT/Z477vThu6snfdkBuhxnChpOKNGxcQ71ow561Qoa6uqLdPtag== @@ -1330,12 +1357,16 @@ node-fetch "^2.6.1" universal-user-agent "^6.0.0" -"@octokit/rest@16.28.7": - version "16.28.7" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.28.7.tgz#a2c2db5b318da84144beba82d19c1a9dbdb1a1fa" - integrity sha512-cznFSLEhh22XD3XeqJw51OLSfyL2fcFKUO+v2Ep9MTAFfFLS1cK1Zwd1yEgQJmJoDnj4/vv3+fGGZweG+xsbIA== +"@octokit/rest@16.43.2": + version "16.43.2" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.43.2.tgz#c53426f1e1d1044dee967023e3279c50993dd91b" + integrity sha512-ngDBevLbBTFfrHZeiS7SAMAZ6ssuVmXuya+F/7RaVvlysgGa1JKJkKWY+jV6TCJYcW0OALfJ7nTIGXcBXzycfQ== dependencies: - "@octokit/request" "^5.0.0" + "@octokit/auth-token" "^2.4.0" + "@octokit/plugin-paginate-rest" "^1.1.1" + "@octokit/plugin-request-log" "^1.0.0" + "@octokit/plugin-rest-endpoint-methods" "2.4.0" + "@octokit/request" "^5.2.0" "@octokit/request-error" "^1.0.2" atob-lite "^2.0.0" before-after-hook "^2.0.0" @@ -1346,10 +1377,9 @@ lodash.uniq "^4.5.0" octokit-pagination-methods "^1.1.0" once "^1.4.0" - universal-user-agent "^3.0.0" - url-template "^2.0.8" + universal-user-agent "^4.0.0" -"@octokit/types@^2.0.0": +"@octokit/types@^2.0.0", "@octokit/types@^2.0.1": version "2.16.2" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-2.16.2.tgz#4c5f8da3c6fecf3da1811aef678fda03edac35d2" integrity sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q== @@ -4660,7 +4690,7 @@ dependency-tree@^8.0.0: precinct "^7.0.0" typescript "^3.9.7" -deprecation@^2.0.0: +deprecation@^2.0.0, deprecation@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== @@ -9714,7 +9744,7 @@ os-locale@^1.4.0: dependencies: lcid "^1.0.0" -os-name@^3.0.0: +os-name@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801" integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg== @@ -13520,12 +13550,12 @@ universal-analytics@^0.4.16: request "^2.88.2" uuid "^3.0.0" -universal-user-agent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-3.0.0.tgz#4cc88d68097bffd7ac42e3b7c903e7481424b4b9" - integrity sha512-T3siHThqoj5X0benA5H0qcDnrKGXzU8TKoX15x/tQHw1hQBvIEBHjxQ2klizYsqBOO/Q+WuxoQUihadeeqDnoA== +universal-user-agent@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557" + integrity sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg== dependencies: - os-name "^3.0.0" + os-name "^3.1.0" universal-user-agent@^6.0.0: version "6.0.0" @@ -13662,11 +13692,6 @@ url-parse@^1.4.3, url-parse@^1.5.1: querystringify "^2.1.1" requires-port "^1.0.0" -url-template@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" - integrity sha1-/FZaPMy/93MMd19WQflVV5FDnyE= - url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"