Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { GetRepositoryMetadataQuery } from "./github/graphql/generated/oper
import {
createCommitOnBranchQuery,
getRepositoryMetadata,
type Octokit,
} from "./github/graphql/queries.ts";
import type { CommitChangesOptions, CommitChangesResult } from "./types.ts";
import { normalizeCommitMessage, resolveGitRef } from "./utils.ts";
Expand All @@ -17,7 +18,7 @@ type CreateCommit = (
* Works in Node.js and browsers.
*/
export async function commitChanges({
octokit,
octokit: partialOctokit,
owner,
repo,
branch,
Expand All @@ -26,6 +27,7 @@ export async function commitChanges({
message,
fileChanges,
}: CommitChangesOptions): Promise<CommitChangesResult> {
const octokit = partialOctokit as Octokit;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I preferred the previous setup - it required some manual redeclaration, but at least it didn't require casts. Even though we were not redeclaring the whole thing the compiler would still catch the type errors nicely.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's wrong with casts? It's not doing an interim as any/unknown and it should also catch type errors equally.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice, this is probably OK-ish but this certainly does allow invalid invocations through the public API. One could pass literal PartialOctokit's equivalent to our public commitChanges export and the compiler would be fine with all of that unknowns but we'd crash at runtime.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How and why would someone do that though? I don't think it's worth the effort to protect an edgecase.

If you feel strongly about this though, would you be ok if we keep the previous manual type as PartialOctokit, and keep the cast to Octokit? The initial goal here is that if internally we can use Octokit, it's easier for us to discover and work on the octokit API if we want to update in the future.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To achieve this goal you can do a "validated casting" like this:

type DeepPartial<T> =
  T extends Function
    ? T
    : T extends object
      ? { [P in keyof T]?: DeepPartial<T[P]> }
      : T;

declare function toOctokit(
  octokit: DeepPartial<Octokit>
): Octokit;

This way if our PartialOctokit stops matching the Octokit the compiler will error at toOctokit call sites. But you should still be able to freely use the return value of this casting helper and experiment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could have such validation in our tests (a type-level test would suffice) - and in the implementation we could then cast freely, without the dummy toOctokit helper

const baseRef = resolveGitRef(base);

const info = await getRepositoryMetadata(octokit, {
Expand Down Expand Up @@ -168,7 +170,10 @@ async function createOrForceUpdateTemporaryBranch({
repo,
tempBranch,
baseSha,
}: Pick<CommitChangesOptions, "octokit" | "owner" | "repo"> & {
}: {
octokit: Octokit;
owner: string;
repo: string;
tempBranch: string;
baseSha: string;
}) {
Expand Down
48 changes: 14 additions & 34 deletions src/github/graphql/queries.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,17 @@
// Octokit types are messy. To avoid adding any (peer)dependencies we rely on TS structural typing here
export type Octokit = {
graphql: <T>(
query: string,
variables?: Record<string, unknown>,
) => Promise<T>;
rest: {
git: {
createRef: (params: {
owner: string;
repo: string;
ref: string;
sha: string;
}) => Promise<{ data: { node_id?: string } }>;
updateRef: (params: {
owner: string;
repo: string;
ref: string;
sha: string;
force?: boolean;
}) => Promise<{ data: { node_id?: string } }>;
deleteRef: (params: {
owner: string;
repo: string;
ref: string;
}) => Promise<unknown>;
getRef?: (params: {
owner: string;
repo: string;
ref: string;
}) => Promise<unknown>;
};
};
};
import type { getOctokit } from "@actions/github";

export type Octokit = ReturnType<typeof getOctokit>;

/**
* Rough shape of the Octokit instance we need so we don't depend on the full
* dependency and types.
*/
export interface PartialOctokit {
request: unknown;
graphql: unknown;
rest: unknown;
paginate: unknown;
}

import type {
CreateCommitOnBranchMutation,
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {
CommitMessage,
FileChanges,
} from "./github/graphql/generated/types.ts";
import type { Octokit } from "./github/graphql/queries.ts";
import type { PartialOctokit } from "./github/graphql/queries.ts";

export type GitRef = { branch: string } | { tag: string } | { commit: string };

Expand All @@ -12,7 +12,7 @@ export interface CommitChangesOptions {
* from `@octokit/core`, `@octokit/plugin-rest-endpoint-methods`, and
* `@octokit/plugin-paginate-rest`.
*/
octokit: Octokit;
octokit: PartialOctokit;
/**
* The owner of the repository.
*/
Expand Down
5 changes: 5 additions & 0 deletions tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export default defineConfig({
clean: !process.argv.includes("--watch"),
deps: {
onlyBundle: [], // require explicitly listing inlined dependencies
neverBundle: [
// Referenced in internal types only and should be treeshaken away, but
// we need to specify it here to allow for linking before treeshaking
"@actions/github",
],
},

sourcemap: !isCi,
Expand Down