4 |
--------------------------------------------------------------------------------
/src/diff/tables/utils.ts:
--------------------------------------------------------------------------------
1 | import {ChangeType, ConsoleLink} from './../types'
2 |
3 | export function consoleLinkFromUrl(url: string): ConsoleLink {
4 | return {
5 | console: {href: url}
6 | }
7 | }
8 |
9 | export function iconFromChangeType(changeType: ChangeType): string {
10 | switch (changeType) {
11 | case 'added':
12 | return '➕'
13 | case 'modified':
14 | return '➕/➖'
15 | case 'deleted':
16 | return '➖'
17 | default:
18 | assertNeverChangeType(changeType)
19 | }
20 | }
21 |
22 | export function assertNeverChangeType(changeType: never): never {
23 | throw new Error(`Unexpected change type: ${changeType}`)
24 | }
25 |
--------------------------------------------------------------------------------
/src/diff/templates.ts:
--------------------------------------------------------------------------------
1 | export const CHANGE_TEMPLATE = `🤖 Hasura Change Summary compared a subset of table metadata including permissions:
2 | {{{version}}}
3 | {{{tables}}}
4 | `
5 |
6 | export const VERSION_TEMPLATE = `Upgraded Config
7 | This project upgraded to config v{{version}}
! Read about what has changed.
`
8 |
--------------------------------------------------------------------------------
/src/diff/types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Columns,
3 | DeletePermissionEntry,
4 | InsertPermissionEntry,
5 | QualifiedTable as QualifiedTableBase,
6 | SelectPermissionEntry,
7 | TableEntry as TableEntryBase,
8 | UpdatePermissionEntry
9 | } from '@hasura/metadata'
10 |
11 | /* #region metadata */
12 | export interface QualifiedTable extends QualifiedTableBase {
13 | database?: string
14 | }
15 |
16 | export interface TableEntry extends TableEntryBase {
17 | table: QualifiedTable
18 | }
19 | /* #endregion */
20 |
21 | /* #region changes */
22 | export const ChangeTypes = ['added', 'modified', 'deleted'] as const
23 |
24 | export type ChangeType = typeof ChangeTypes[number]
25 |
26 | export interface ConsoleLink {
27 | console: {
28 | href: string
29 | }
30 | }
31 |
32 | /**
33 | * Resembles Hypertext Application Language (HAL)
34 | *
35 | * @see https://stateless.group/hal_specification.html
36 | */
37 | export interface LinkableChange {
38 | _links?: ConsoleLink
39 | }
40 |
41 | export const TablePermissions = [
42 | 'insert_permissions',
43 | 'select_permissions',
44 | 'update_permissions',
45 | 'delete_permissions'
46 | ] as const
47 |
48 | export type TablePermission = typeof TablePermissions[number]
49 |
50 | /* #region TablePermissionColumns */
51 | export interface TablePermissionColumn {
52 | name: string
53 | isComputed: boolean
54 | }
55 |
56 | export type TablePermissionColumnAll = Columns.Empty
57 |
58 | export type TablePermissionColumns =
59 | | TablePermissionColumn[]
60 | | TablePermissionColumnAll
61 | /* #endregion */
62 |
63 | /* #region TablePermissionColumnChanges */
64 | export interface TablePermissionColumnChange extends TablePermissionColumn {
65 | type: 'added' | 'deleted'
66 | }
67 |
68 | /** `true` denotes column permissions changed in some way, but the explicit set of columns is unknown */
69 | export type TablePermissionColumnChanges = TablePermissionColumnChange[] | true
70 | /* #endregion */
71 |
72 | export interface TablePermissionChange {
73 | role: string
74 | columns: TablePermissionColumnChanges
75 | }
76 |
77 | export type TablePermissionChanges = Record
78 |
79 | export type TablePermissionsChanges = Record<
80 | TablePermission,
81 | TablePermissionChanges
82 | >
83 |
84 | export type TableChange = QualifiedTable & LinkableChange
85 |
86 | export interface TableEntryChange extends TablePermissionsChanges {
87 | table: TableChange
88 | }
89 |
90 | export type TableEntryChanges = Record
91 |
92 | export type VersionChange = 3 | undefined
93 |
94 | export interface Changes {
95 | version: VersionChange
96 | tables: TableEntryChanges
97 | }
98 | /* #endregion */
99 |
100 | export type PermissionEntry =
101 | | InsertPermissionEntry
102 | | SelectPermissionEntry
103 | | UpdatePermissionEntry
104 | | DeletePermissionEntry
105 |
106 | /* #region jsondiffpatch */
107 | export type DeltaAddition = [T]
108 |
109 | export type DeltaDeletion = [T, 0, 0]
110 |
111 | export type DeltaModificationConventional = [T, T]
112 |
113 | /** @see https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md */
114 | export type Delta =
115 | | DeltaAddition
116 | | DeltaDeletion
117 | | DeltaModificationConventional
118 | /* #endregion */
119 |
120 | export interface DiffOptions {
121 | /** Hasura GraphQL engine http(s) endpoint, used for deep console links */
122 | hasuraEndpoint?: string
123 | }
124 |
--------------------------------------------------------------------------------
/src/diff/version.ts:
--------------------------------------------------------------------------------
1 | import * as core from '@actions/core'
2 |
3 | import {ConvertedMetadataVersion} from '../load/types'
4 | import {VERSION_TEMPLATE} from './templates'
5 | import {VersionChange} from './types'
6 | import {renderTemplate} from './functions'
7 |
8 | export function diffVersion(
9 | oldConvertedFrom: ConvertedMetadataVersion,
10 | newConvertedFrom: ConvertedMetadataVersion
11 | ): VersionChange {
12 | if (2 === oldConvertedFrom && !newConvertedFrom) {
13 | return 3
14 | }
15 | }
16 |
17 | export function formatVersion(version: VersionChange): string {
18 | if (!version) {
19 | return ''
20 | }
21 |
22 | core.info('Formatting version change')
23 |
24 | return renderTemplate(VERSION_TEMPLATE, {version}).trim()
25 | }
26 |
--------------------------------------------------------------------------------
/src/load/AbstractMetadataLoader.ts:
--------------------------------------------------------------------------------
1 | import * as core from '@actions/core'
2 |
3 | import {
4 | HasuraMetadata,
5 | HasuraMetadataLatest,
6 | MetadataLoader,
7 | MetadataProperty
8 | } from './types'
9 | import {
10 | convertMetadataToLatest,
11 | metadataFilenameFromProperty,
12 | metadataFromVersion,
13 | metadataFromVersionContents
14 | } from './functions'
15 |
16 | import {join} from 'path'
17 | import {load} from './yaml'
18 |
19 | export abstract class AbstractMetadataLoader implements MetadataLoader {
20 | protected abstract readFile(path: string): Promise
21 |
22 | async load(
23 | projectDir: string,
24 | emptyFallback = false
25 | ): Promise {
26 | let metadata
27 |
28 | try {
29 | core.info('Initializing metadata from version')
30 | metadata = metadataFromVersionContents(
31 | await this.readFile(this.filePathFromProperty(projectDir, 'version'))
32 | )
33 | } catch (error) {
34 | if (emptyFallback) {
35 | return convertMetadataToLatest(metadataFromVersion(3))
36 | }
37 |
38 | throw error
39 | }
40 |
41 | const metadataProperties = Object.keys(metadata).filter(
42 | key => 'version' !== key
43 | ) as MetadataProperty[]
44 |
45 | for (const property of metadataProperties) {
46 | core.info(`Parsing ${property} YAML metadata`)
47 | metadata[property as keyof HasuraMetadata] = await load(
48 | this.filePathFromProperty(projectDir, property),
49 | this.readFile.bind(this)
50 | )
51 | }
52 |
53 | return convertMetadataToLatest(metadata)
54 | }
55 |
56 | protected metadataPathFromProject(projectDir: string): string {
57 | return join(projectDir, 'metadata')
58 | }
59 |
60 | private filePathFromProperty(
61 | projectDir: string,
62 | property: MetadataProperty
63 | ): string {
64 | return join(
65 | this.metadataPathFromProject(projectDir),
66 | metadataFilenameFromProperty(property)
67 | )
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/load/GitHubLoader.ts:
--------------------------------------------------------------------------------
1 | import * as core from '@actions/core'
2 |
3 | import {basename, dirname} from 'path'
4 | import {
5 | isTreeEntryBlob,
6 | MetadataContentsGraphqlResponse,
7 | TreeEntryBlob
8 | } from './types'
9 |
10 | import {Context} from '@actions/github/lib/context'
11 | import {GitHub} from '@actions/github/lib/utils'
12 | import {isArray} from 'lodash'
13 | import {AbstractMetadataLoader} from './AbstractMetadataLoader'
14 | import {METADATA_CONTENTS_GRAPHQL_QUERY} from './consts'
15 |
16 | export class GitHubLoader extends AbstractMetadataLoader {
17 | private directoryContents: Map = new Map()
18 |
19 | constructor(
20 | private octokit: InstanceType,
21 | private repo: Context['repo'],
22 | private baseRef: string
23 | ) {
24 | super()
25 | }
26 |
27 | protected async readFile(path: string): Promise {
28 | const entries = await this.fetchDirectoryContents(dirname(path))
29 |
30 | const filename = basename(path)
31 | const entry = entries.find(({name}) => name === filename)
32 |
33 | if (!entry) {
34 | throw new Error(`Error reading file: ${path}`)
35 | }
36 |
37 | return entry.object.text
38 | }
39 |
40 | private async fetchDirectoryContents(
41 | directory: string
42 | ): Promise {
43 | if (!this.directoryContents.has(directory)) {
44 | const objectExpression = `${this.baseRef}:${directory}`
45 |
46 | core.debug(`Fetching directory contents: ${objectExpression}`)
47 | const {repository} =
48 | await this.octokit.graphql(
49 | METADATA_CONTENTS_GRAPHQL_QUERY,
50 | {
51 | ...this.repo,
52 | objectExpression
53 | }
54 | )
55 |
56 | const entries = repository.object?.entries
57 |
58 | this.directoryContents.set(
59 | directory,
60 | isArray(entries) ? entries.filter(isTreeEntryBlob) : []
61 | )
62 | }
63 |
64 | return this.directoryContents.get(directory) ?? []
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/load/WorkspaceLoader.ts:
--------------------------------------------------------------------------------
1 | import * as core from '@actions/core'
2 |
3 | import {AbstractMetadataLoader} from './AbstractMetadataLoader'
4 | import {join} from 'path'
5 | import {readFileSync} from 'fs'
6 |
7 | export class WorkspaceLoader extends AbstractMetadataLoader {
8 | constructor(private workspacePath: string) {
9 | super()
10 | }
11 |
12 | protected metadataPathFromProject(projectDir: string): string {
13 | return join(this.workspacePath, super.metadataPathFromProject(projectDir))
14 | }
15 |
16 | protected async readFile(path: string): Promise {
17 | core.debug(`Reading file: ${path}`)
18 | return readFileSync(path, 'utf8')
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/load/__tests__/GitHubLoader.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import {HasuraMetadata, TreeEntry} from '../types'
3 | import {lstatSync, readFileSync, readdirSync} from 'fs'
4 |
5 | import {GitHubLoader} from '../GitHubLoader'
6 | import type {RequestParameters} from '@octokit/types'
7 | import {isString} from 'lodash'
8 | import {join} from 'path'
9 |
10 | describe('GitHubLoader', () => {
11 | let target: GitHubLoader
12 | let metadata: HasuraMetadata
13 | let octokit: any
14 |
15 | beforeEach(() => {
16 | octokit = {}
17 |
18 | target = new GitHubLoader(
19 | octokit,
20 | {
21 | owner: 'OWNER',
22 | repo: 'REPO'
23 | },
24 | 'main'
25 | )
26 | })
27 |
28 | describe('load v2', () => {
29 | describe('empty', () => {
30 | beforeEach(async () => {
31 | octokit.graphql = jest.fn(() => ({
32 | repository: {
33 | object: null
34 | }
35 | }))
36 |
37 | metadata = await target.load('src', true)
38 | })
39 |
40 | it('should make GraphQL call', () => {
41 | expect(octokit.graphql).toHaveBeenCalledWith(expect.any(String), {
42 | owner: 'OWNER',
43 | repo: 'REPO',
44 | objectExpression: 'main:src/metadata'
45 | })
46 | })
47 |
48 | it('should return empty metadata', () => {
49 | expect(metadata).toStrictEqual({
50 | version: 3,
51 | databases: []
52 | })
53 | })
54 | })
55 |
56 | describe('existing', () => {
57 | beforeEach(async () => {
58 | octokit.graphql = jest.fn(() => ({
59 | repository: {
60 | object: {
61 | entries: treeEntryFixtures(
62 | join('__tests__', 'fixtures', 'v2', 'metadata')
63 | )
64 | }
65 | }
66 | }))
67 |
68 | metadata = await target.load('.')
69 | })
70 |
71 | it('should make GraphQL call', () => {
72 | expect(octokit.graphql).toHaveBeenCalledWith(expect.any(String), {
73 | owner: 'OWNER',
74 | repo: 'REPO',
75 | objectExpression: 'main:metadata'
76 | })
77 | })
78 |
79 | it('should return metadata', () => {
80 | expect(metadata).toStrictEqual({
81 | __converted_from: 2,
82 | version: 3,
83 | databases: [
84 | {
85 | name: 'default',
86 | tables: [
87 | {
88 | table: {
89 | schema: 'public',
90 | name: 'users'
91 | }
92 | }
93 | ]
94 | }
95 | ]
96 | })
97 | })
98 | })
99 | })
100 |
101 | describe('load v3', () => {
102 | beforeEach(async () => {
103 | octokit.graphql = jest.fn(
104 | (_query: string, {objectExpression}: RequestParameters) => {
105 | if (!isString(objectExpression)) {
106 | throw new Error('objectExpression is not a string')
107 | }
108 |
109 | const directory = objectExpression.slice('main:'.length)
110 |
111 | return {
112 | repository: {
113 | object: {
114 | entries: treeEntryFixtures(
115 | join('__tests__', 'fixtures', 'v3', directory)
116 | )
117 | }
118 | }
119 | }
120 | }
121 | )
122 |
123 | metadata = await target.load('.')
124 | })
125 |
126 | const expectedObjectExpression: string[] = [
127 | 'main:metadata',
128 | 'main:metadata/databases',
129 | 'main:metadata/databases/default/tables'
130 | ]
131 |
132 | it(`should make ${expectedObjectExpression.length} GraphQL calls`, () => {
133 | for (const objectExpression of expectedObjectExpression) {
134 | expect(octokit.graphql).toHaveBeenCalledWith(expect.any(String), {
135 | owner: 'OWNER',
136 | repo: 'REPO',
137 | objectExpression
138 | })
139 | }
140 |
141 | expect(octokit.graphql.mock.calls.length).toStrictEqual(
142 | expectedObjectExpression.length
143 | )
144 | })
145 |
146 | it('should return metadata', () => {
147 | expect(metadata).toStrictEqual({
148 | version: 3,
149 | databases: [
150 | {
151 | name: 'default',
152 | kind: 'postgres',
153 | tables: [
154 | {
155 | table: {
156 | schema: 'public',
157 | name: 'users'
158 | }
159 | }
160 | ]
161 | }
162 | ]
163 | })
164 | })
165 | })
166 | })
167 |
168 | function treeEntryFixtures(directory: string): TreeEntry[] {
169 | return readdirSync(directory).map>(name => {
170 | const path = join(directory, name)
171 |
172 | if (lstatSync(path).isFile()) {
173 | return {
174 | name,
175 | object: {text: readFileSync(path, 'utf8')}
176 | }
177 | }
178 |
179 | return {name, object: {}}
180 | })
181 | }
182 |
--------------------------------------------------------------------------------
/src/load/__tests__/WorkspaceLoader.test.ts:
--------------------------------------------------------------------------------
1 | import {WorkspaceLoader} from '../WorkspaceLoader'
2 |
3 | describe('WorkspaceLoader', () => {
4 | let target: WorkspaceLoader
5 |
6 | beforeEach(() => {
7 | target = new WorkspaceLoader('./')
8 | })
9 |
10 | test('load v2', async () => {
11 | const metadata = await target.load('./__tests__/fixtures/v2')
12 |
13 | expect(metadata).toStrictEqual({
14 | __converted_from: 2,
15 | version: 3,
16 | databases: [
17 | {
18 | name: 'default',
19 | tables: [
20 | {
21 | table: {
22 | schema: 'public',
23 | name: 'users'
24 | }
25 | }
26 | ]
27 | }
28 | ]
29 | })
30 | })
31 |
32 | test('load v3', async () => {
33 | const metadata = await target.load('./__tests__/fixtures/v3')
34 |
35 | expect(metadata).toStrictEqual({
36 | version: 3,
37 | databases: [
38 | {
39 | name: 'default',
40 | kind: 'postgres',
41 | tables: [
42 | {
43 | table: {
44 | schema: 'public',
45 | name: 'users'
46 | }
47 | }
48 | ]
49 | }
50 | ]
51 | })
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/src/load/consts.ts:
--------------------------------------------------------------------------------
1 | import {Database, MetadataProperty} from './types'
2 |
3 | export const METADATA_PROPERTIES: MetadataProperty[] = ['databases']
4 |
5 | export const DEFAULT_DATABASE_NAME = 'default'
6 |
7 | export const DEFAULT_DATABASE: Database = {
8 | name: DEFAULT_DATABASE_NAME,
9 | tables: []
10 | }
11 |
12 | export const METADATA_CONTENTS_GRAPHQL_QUERY = `
13 | query metadataContents($owner: String!, $repo: String!, $objectExpression: String!) {
14 | repository(owner: $owner, name: $repo) {
15 | object(expression: $objectExpression) {
16 | ... on Tree {
17 | entries {
18 | name
19 | object {
20 | ... on Blob {
21 | text
22 | }
23 | }
24 | }
25 | }
26 | }
27 | }
28 | }`
29 |
--------------------------------------------------------------------------------
/src/load/functions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | HasuraMetadata,
3 | HasuraMetadataLatest,
4 | MetadataProperty,
5 | isMetadataV2,
6 | isMetadataV3
7 | } from './types'
8 |
9 | import {DEFAULT_DATABASE} from './consts'
10 | import {isObject} from 'lodash'
11 | import {load} from 'js-yaml'
12 |
13 | export function metadataFilenameFromProperty(
14 | property: MetadataProperty
15 | ): string {
16 | const filename = `${property}.yaml`
17 |
18 | if ('databases' === property) {
19 | return `${property}/${filename}`
20 | }
21 |
22 | return filename
23 | }
24 |
25 | export function metadataFromVersionContents(
26 | fileContents: string
27 | ): HasuraMetadata {
28 | const metadata = load(fileContents) as HasuraMetadata
29 |
30 | if (!isObject(metadata) || !isFinite(metadata.version)) {
31 | throw new Error('Invalid version metadata file')
32 | }
33 |
34 | return metadataFromVersion(metadata.version)
35 | }
36 |
37 | export function metadataFromVersion(version: number): HasuraMetadata {
38 | switch (version) {
39 | case 2:
40 | return {
41 | version,
42 | tables: []
43 | }
44 | case 3:
45 | return {
46 | version,
47 | databases: []
48 | }
49 | default:
50 | throw new Error('Unsupported metadata version')
51 | }
52 | }
53 |
54 | export function convertMetadataToLatest(
55 | metadata: HasuraMetadata
56 | ): HasuraMetadataLatest {
57 | if (isMetadataV2(metadata)) {
58 | return {
59 | __converted_from: 2,
60 | version: 3,
61 | databases: [
62 | {
63 | ...DEFAULT_DATABASE,
64 | tables: metadata.tables
65 | }
66 | ]
67 | }
68 | }
69 |
70 | if (isMetadataV3(metadata)) {
71 | return metadata
72 | }
73 |
74 | return assertNever(metadata)
75 | }
76 |
77 | function assertNever(metadata: never): never {
78 | throw new Error(`Unexpected metadata: ${JSON.stringify(metadata)}`)
79 | }
80 |
--------------------------------------------------------------------------------
/src/load/types.ts:
--------------------------------------------------------------------------------
1 | import {HasuraMetadataV2, TableEntry} from '@hasura/metadata'
2 | import {isObject, isString} from 'lodash'
3 |
4 | export interface Database {
5 | name: string
6 | tables: TableEntry[]
7 | }
8 |
9 | export interface HasuraMetadataV3 extends Omit {
10 | version: 3
11 | databases: Database[]
12 | }
13 |
14 | export type ConvertedMetadataVersion = 2 | undefined
15 |
16 | export interface HasuraMetadataLatest extends HasuraMetadataV3 {
17 | __converted_from?: ConvertedMetadataVersion
18 | }
19 |
20 | export type HasuraMetadata = HasuraMetadataV2 | HasuraMetadataV3
21 |
22 | type KeysOfUnion = T extends T ? keyof T : never
23 |
24 | export type MetadataProperty = KeysOfUnion
25 |
26 | export interface MetadataLoader {
27 | /**
28 | * @param emptyFallback default to empty metadata when version cannot be read
29 | */
30 | load(
31 | projectDir: string,
32 | emptyFallback: boolean
33 | ): Promise
34 | }
35 |
36 | export type FileReader = (path: string) => Promise
37 |
38 | export function isMetadataV2(
39 | metadata: HasuraMetadata
40 | ): metadata is HasuraMetadataV2 {
41 | return 2 === metadata.version
42 | }
43 |
44 | export function isMetadataV3(
45 | metadata: HasuraMetadata
46 | ): metadata is HasuraMetadataV3 {
47 | return 3 === metadata.version
48 | }
49 |
50 | export interface TreeEntry {
51 | name: string
52 | object: T
53 | }
54 |
55 | export type TreeEntryBlob = TreeEntry<{
56 | text: string
57 | }>
58 |
59 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
60 | export function isTreeEntryBlob(entry: TreeEntry): entry is TreeEntryBlob {
61 | return (
62 | isObject(entry) &&
63 | isObject(entry.object) &&
64 | isString((entry.object as {text: string}).text)
65 | )
66 | }
67 |
68 | export interface MetadataContentsGraphqlResponse {
69 | repository: {
70 | object?: {
71 | entries?: TreeEntryBlob[]
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/load/yaml.ts:
--------------------------------------------------------------------------------
1 | import {dirname, join} from 'path'
2 | import {isArray, isObject, isString} from 'lodash'
3 |
4 | import {FileReader} from './types'
5 | import yaml from 'js-yaml'
6 |
7 | const INCLUDE_PREFIX = '!include '
8 |
9 | export async function load(path: string, read: FileReader): Promise {
10 | const str = await read(path)
11 |
12 | return loadContent(yaml.load(str), dirname(path), read)
13 | }
14 |
15 | export async function loadContent(
16 | content: unknown,
17 | directory: string,
18 | read: FileReader
19 | ): Promise {
20 | const loadArrayValue = async (value: unknown): Promise => {
21 | const includePath = includePathFromValue(value)
22 |
23 | if (includePath) {
24 | const path = join(directory, includePath)
25 |
26 | return loadContent(yaml.load(await read(path)), dirname(path), read)
27 | }
28 |
29 | return loadContent(value, directory, read)
30 | }
31 |
32 | if (isArray(content)) {
33 | const result = []
34 |
35 | for (const value of content) {
36 | result.push(await loadArrayValue(value))
37 | }
38 |
39 | return result as unknown as T
40 | }
41 |
42 | if (isObject(content)) {
43 | for (const [key, value] of Object.entries(content)) {
44 | const includePath = includePathFromValue(value)
45 |
46 | if (includePath) {
47 | const path = join(directory, includePath)
48 |
49 | ;(content as Record)[key] = await loadContent(
50 | yaml.load(await read(path)),
51 | dirname(path),
52 | read
53 | )
54 | }
55 | }
56 | }
57 |
58 | return content as T
59 | }
60 |
61 | export function includePathFromValue(value: unknown): string | undefined {
62 | if (isString(value) && value.startsWith(INCLUDE_PREFIX)) {
63 | return value.substring(INCLUDE_PREFIX.length)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import * as core from '@actions/core'
2 | import * as github from '@actions/github'
3 |
4 | import {diff, format} from './diff'
5 |
6 | import {GitHubLoader} from './load/GitHubLoader'
7 | import {WorkspaceLoader} from './load/WorkspaceLoader'
8 |
9 | async function run(): Promise {
10 | try {
11 | const projectDir = core.getInput('project_dir')
12 |
13 | core.startGroup(`Loading old metadata in: ${projectDir}`)
14 | const oldMetadata = await new GitHubLoader(
15 | github.getOctokit(core.getInput('github_token')),
16 | github.context.repo,
17 | process.env.GITHUB_BASE_REF ?? ''
18 | ).load(projectDir, true)
19 | core.endGroup()
20 |
21 | core.startGroup(`Loading new metadata in: ${projectDir}`)
22 | const newMetadata = await new WorkspaceLoader(
23 | process.env.GITHUB_WORKSPACE ?? ''
24 | ).load(projectDir)
25 | core.endGroup()
26 |
27 | const changes = diff(oldMetadata, newMetadata, {
28 | hasuraEndpoint: core.getInput('hasura_endpoint')
29 | })
30 | const changeHtml = format(changes)
31 |
32 | core.info('Writing job summary')
33 | await core.summary.addRaw(changeHtml).addEOL().write()
34 |
35 | core.setOutput('change_html', changeHtml)
36 | } catch (error) {
37 | core.setFailed(error instanceof Error ? error.message : String(error))
38 |
39 | if (error instanceof Error && error.stack) {
40 | core.debug(error.stack)
41 | }
42 | }
43 | }
44 |
45 | run()
46 |
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
4 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
5 | "outDir": "./lib" /* Redirect output structure to the directory. */,
6 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
7 | "strict": true /* Enable all strict type-checking options. */,
8 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
9 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
10 | },
11 | "exclude": ["node_modules", "**/*.test.ts"]
12 | }
13 |
--------------------------------------------------------------------------------