-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[HERMES-1721] Add check-update hook functionality (#10)
- Loading branch information
Showing
8 changed files
with
365 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# Maintainers Guide | ||
|
||
This document describes tools, tasks and workflow that one needs to be familiar with in order to effectively maintain | ||
this project. If you use this package within your own software as is but don't plan on modifying it, this guide is | ||
**not** for you. | ||
|
||
## Tools | ||
|
||
You will need [Deno](https://deno.land). | ||
|
||
## Tasks | ||
|
||
### Testing | ||
|
||
This package has unit tests in the `src/tests` directory. You can run the entire test suite via: | ||
|
||
deno test --allow-read --allow-env --coverage=.coverage | ||
|
||
To run the tests along with a coverage report: | ||
|
||
deno test --allow-read --allow-env --coverage=.coverage && deno coverage --exclude="fixtures|test" .coverage | ||
|
||
This command is also executed by GitHub Actions, the continuous integration service, for every Pull Request and branch. | ||
|
||
### Linting and Formatting | ||
|
||
This package adheres to deno lint and formatting standards. To ensure the code base adheres to these standards, run the following commands: | ||
|
||
deno lint ./src | ||
deno fmt ./src | ||
|
||
Any warnings and errors must be addressed. | ||
|
||
### Releasing | ||
|
||
Releasing can feel intimidating at first, but rest assured: if you make a mistake, don't fret! We can always roll forward with another release 😃 | ||
|
||
1. Make sure your local `main` branch has the latest changes. | ||
2. Run the tests as per the above Testing section, and any other local verification, such as: | ||
- Local integration tests between the Slack CLI, deno-sdk-based application template(s) and this repo. One can modify a deno-sdk-based app project's `slack.json` file to point the `get-hooks` hook to a local version of this repo rather than the deno.land-hosted version. | ||
3. Bump the version number for this repo in adherence to [Semantic Versioning](http://semver.org/) in `src/libraries.ts`, specifically the `VERSIONS` map's `DENO_SLACK_HOOKS` key. | ||
- Make a single commit with a message for the version bump. | ||
4. Tag the version commit with a tag matching the version number. I.e. if you are releasing version 1.2.3 of this repo, then the git tag should be `1.2.3`. | ||
- This can be done with the command: `git tag 1.2.3` | ||
5. Push the commit and tag to GitHub: `git push --tags origin main`. This will kick off an automatic deployment to https://deno.land/x/deno_slack_hooks | ||
6. Create a GitHub Release based on the newly-created tag with release notes. | ||
- From the repository, navigate to the **Releases** section and draft a new release. You can use prior releases as a template. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
.DS_Store | ||
.coverage | ||
.coverage | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
import { | ||
DENO_SLACK_API, | ||
DENO_SLACK_HOOKS, | ||
DENO_SLACK_SDK, | ||
} from "./libraries.ts"; | ||
|
||
const IMPORT_MAP_SDKS = [DENO_SLACK_SDK, DENO_SLACK_API]; | ||
const SLACK_JSON_SDKS = [ | ||
DENO_SLACK_HOOKS, // should be the only one needed now that the get-hooks hook is supported | ||
]; | ||
|
||
interface UpdateResponse { | ||
name: string; | ||
releases: Release[]; | ||
message?: string; | ||
url?: string; | ||
error?: { | ||
message: string; | ||
} | null; | ||
} | ||
|
||
interface VersionMap { | ||
[key: string]: Release; | ||
} | ||
|
||
interface Release { | ||
name: string; | ||
current?: string; | ||
latest?: string; | ||
update?: boolean; | ||
breaking?: boolean; | ||
message?: string; | ||
url?: string; | ||
error?: { | ||
message: string; | ||
} | null; | ||
} | ||
|
||
export const checkForSDKUpdates = async () => { | ||
const versionMap: VersionMap = await createVersionMap(); | ||
const updateResp = createUpdateResp(versionMap); | ||
return updateResp; | ||
}; | ||
|
||
/** | ||
* createVersionMap creates an object that contains each dependency, | ||
* featuring information about the current and latest versions, as well | ||
* as if breaking changes are present and if any errors occurred during | ||
* version retrieval. | ||
*/ | ||
async function createVersionMap() { | ||
const versionMap: VersionMap = await readProjectDependencies(); | ||
|
||
// Check each dependency for updates, classify update as breaking or not, | ||
// craft message with information retrieved, and note any error that occurred. | ||
for (const [sdk, value] of Object.entries(versionMap)) { | ||
if (value) { | ||
const current = versionMap[sdk].current || ""; | ||
let latest = "", error = null; | ||
|
||
try { | ||
latest = await fetchLatestModuleVersion(sdk); | ||
} catch (err) { | ||
error = err; | ||
} | ||
|
||
const update = (!!current && !!latest) && current !== latest; | ||
const breaking = hasBreakingChange(current, latest); | ||
|
||
versionMap[sdk] = { | ||
...versionMap[sdk], | ||
latest, | ||
update, | ||
breaking, | ||
error, | ||
}; | ||
} | ||
} | ||
|
||
return versionMap; | ||
} | ||
|
||
/** readProjectDependencies reads from possible dependency files | ||
* (import_map.json, slack.json) and maps them to the versionMap | ||
* containing each dependency's update information | ||
*/ | ||
async function readProjectDependencies(): Promise<VersionMap> { | ||
const cwd = Deno.cwd(); | ||
const versionMap: VersionMap = {}; | ||
|
||
// Find SDK component versions in import map, if available | ||
const map = await getJson(`${cwd}/import_map.json`); | ||
for (const sdkUrl of Object.values(map.imports) as string[]) { | ||
for (const sdk of IMPORT_MAP_SDKS) { | ||
if (sdkUrl.includes(sdk)) { | ||
versionMap[sdk] = { | ||
name: sdk, | ||
current: extractVersion(sdkUrl), | ||
}; | ||
} | ||
} | ||
} | ||
|
||
// Find SDK component versions in slack.json, if available | ||
const { hooks }: { [key: string]: string } = await getJson( | ||
`${cwd}/slack.json`, | ||
); | ||
for (const command of Object.values(hooks)) { | ||
for (const sdk of SLACK_JSON_SDKS) { | ||
if (command.includes(sdk)) { | ||
versionMap[sdk] = { | ||
name: sdk, | ||
current: extractVersion(command), | ||
}; | ||
} | ||
} | ||
} | ||
|
||
return versionMap; | ||
} | ||
|
||
/** | ||
* getJson attempts to read the given file. If successful, | ||
* it returns an object of the contained JSON. If the extraction | ||
* fails, it returns an empty object. | ||
*/ | ||
async function getJson(file: string) { | ||
try { | ||
return JSON.parse(await Deno.readTextFile(file)); | ||
} catch (_) { | ||
return {}; | ||
} | ||
} | ||
|
||
/** fetchLatestModuleVersion makes a call to deno.land with the | ||
* module name and returns the extracted version number, if found | ||
*/ | ||
export async function fetchLatestModuleVersion( | ||
moduleName: string, | ||
): Promise<string> { | ||
const res = await fetch(`https://deno.land/x/${moduleName}`, { | ||
redirect: "manual", | ||
}); | ||
|
||
const redirect = res.headers.get("location"); | ||
if (redirect === null) { | ||
throw new Error(`${moduleName} not found on deno.land!`); | ||
} | ||
|
||
return extractVersion(redirect); | ||
} | ||
|
||
export function extractVersion(str: string) { | ||
const at = str.indexOf("@"); | ||
|
||
// Doesn't contain an @ version | ||
if (at === -1) return ""; | ||
|
||
const slash = str.indexOf("/", at); | ||
const version = slash < at | ||
? str.substring(at + 1) | ||
: str.substring(at + 1, slash); | ||
return version; | ||
} | ||
|
||
/** | ||
* hasBreakingChange determines whether or not there is a | ||
* major version difference of greater or equal to 1 between the current | ||
* and latest version. | ||
*/ | ||
function hasBreakingChange(current: string, latest: string): boolean { | ||
const currMajor = current.split(".")[0]; | ||
const latestMajor = latest.split(".")[0]; | ||
return +latestMajor - +currMajor >= 1; | ||
} | ||
|
||
/** | ||
* createUpdateResp creates and returns an UpdateResponse object | ||
* that contains information about a collection of release dependencies | ||
* in the shape of an object that the CLI expects to consume | ||
*/ | ||
function createUpdateResp(versionMap: VersionMap): UpdateResponse { | ||
const name = "the Slack SDK"; | ||
const releases = []; | ||
const message = ""; | ||
const url = "https://api.slack.com/future/changelog"; | ||
|
||
let error = null; | ||
let errorMsg = ""; | ||
|
||
// Output information for each dependency | ||
for (const sdk of Object.values(versionMap)) { | ||
// Dependency has an update OR the fetch of update failed | ||
if (sdk && (sdk.update || sdk.error?.message)) { | ||
releases.push(sdk); | ||
|
||
// Add the dependency that failed to be fetched to the top-level error message | ||
if (sdk.error && sdk.error.message) { | ||
errorMsg += errorMsg | ||
? `, ${sdk}` | ||
: `An error occurred fetching updates for the following packages: ${sdk.name}`; | ||
} | ||
} | ||
} | ||
|
||
if (errorMsg) error = { message: errorMsg }; | ||
|
||
return { | ||
name, | ||
message, | ||
releases, | ||
url, | ||
error, | ||
}; | ||
} | ||
|
||
if (import.meta.main) { | ||
console.log(JSON.stringify(await checkForSDKUpdates())); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,6 @@ | ||
export { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts"; | ||
export { | ||
assertEquals, | ||
assertRejects, | ||
assertStringIncludes, | ||
} from "https://deno.land/[email protected]/testing/asserts.ts"; | ||
export * from "https://deno.land/x/[email protected]/mod.ts"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
export const DENO_SLACK_SDK = "deno_slack_sdk"; | ||
export const DENO_SLACK_BUILDER = "deno_slack_builder"; | ||
export const DENO_SLACK_API = "deno_slack_api"; | ||
export const DENO_SLACK_HOOKS = "deno_slack_hooks"; | ||
export const DENO_SLACK_RUNTIME = "deno_slack_runtime"; | ||
|
||
export const VERSIONS = { | ||
[DENO_SLACK_BUILDER]: "0.0.12", | ||
[DENO_SLACK_RUNTIME]: "0.0.6", | ||
[DENO_SLACK_HOOKS]: "0.0.6", | ||
}; | ||
|
||
export const BUILDER_TAG = `${DENO_SLACK_BUILDER}@${ | ||
VERSIONS[DENO_SLACK_BUILDER] | ||
}`; | ||
export const RUNTIME_TAG = `${DENO_SLACK_RUNTIME}@${ | ||
VERSIONS[DENO_SLACK_RUNTIME] | ||
}`; | ||
export const HOOKS_TAG = `${DENO_SLACK_HOOKS}@${VERSIONS[DENO_SLACK_HOOKS]}`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,6 @@ | ||
import { BUILDER_TAG, HOOKS_TAG, RUNTIME_TAG } from "./libraries.ts"; | ||
import { getStartHookAdditionalDenoFlags } from "./flags.ts"; | ||
|
||
export const BUILDER_TAG = "[email protected]"; | ||
export const RUNTIME_TAG = "[email protected]"; | ||
|
||
export const projectScripts = (args: string[]) => { | ||
const startHookFlags = getStartHookAdditionalDenoFlags(args); | ||
return { | ||
|
@@ -14,6 +12,8 @@ export const projectScripts = (args: string[]) => { | |
`deno run -q --config=deno.jsonc --allow-read --allow-write --allow-net --allow-run https://deno.land/x/${BUILDER_TAG}/mod.ts`, | ||
"start": | ||
`deno run -q --config=deno.jsonc --allow-read --allow-net ${startHookFlags} https://deno.land/x/${RUNTIME_TAG}/local-run.ts`, | ||
"check-update": | ||
`deno run -q --config=deno.jsonc --allow-read --allow-net https://deno.land/x/${HOOKS_TAG}/check-update.ts`, | ||
}, | ||
"config": { | ||
"watch": { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { assertEquals, assertRejects } from "../dev_deps.ts"; | ||
import * as mf from "../dev_deps.ts"; | ||
import { extractVersion, fetchLatestModuleVersion } from "../check-update.ts"; | ||
|
||
Deno.test("check-update hook tests", async (t) => { | ||
await t.step("extractVersion method", async (evT) => { | ||
await evT.step( | ||
"if version string does not contain an '@' then return empty", | ||
() => { | ||
assertEquals( | ||
extractVersion("bat country"), | ||
"", | ||
"empty string not returned", | ||
); | ||
}, | ||
); | ||
|
||
await evT.step( | ||
"if version string contains a slash after the '@' should return just the version", | ||
() => { | ||
assertEquals( | ||
extractVersion("https://deon.land/x/[email protected]/mod.ts"), | ||
"0.1.0", | ||
"version not returned", | ||
); | ||
}, | ||
); | ||
|
||
await evT.step( | ||
"if version string does not contain a slash after the '@' should return just the version", | ||
() => { | ||
assertEquals( | ||
extractVersion("https://deon.land/x/[email protected]"), | ||
"0.1.0", | ||
"version not returned", | ||
); | ||
}, | ||
); | ||
}); | ||
await t.step("fetchLatestModuleVersion method", async (evT) => { | ||
mf.install(); // mock out calls to fetch | ||
await evT.step( | ||
"should throw if location header is not returned", | ||
async () => { | ||
mf.mock("GET@/x/slack", (_req: Request) => { | ||
return new Response(null, { headers: {} }); | ||
}); | ||
await assertRejects(async () => { | ||
return await fetchLatestModuleVersion("slack"); | ||
}); | ||
}, | ||
); | ||
await evT.step( | ||
"should return version extracted from location header", | ||
async () => { | ||
mf.mock("GET@/x/slack", (_req: Request) => { | ||
return new Response(null, { | ||
headers: { location: "/x/[email protected]" }, | ||
}); | ||
}); | ||
const version = await fetchLatestModuleVersion("slack"); | ||
assertEquals(version, "0.1.1", "inocrrect version returned"); | ||
}, | ||
); | ||
mf.uninstall(); | ||
}); | ||
}); |