Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ jobs:
with:
fetch-depth: 0
- uses: denoland/setup-deno@v2
- run: "deno run --allow-net --allow-env --allow-run shipit.ts ${{ github.ref_name != 'main' && '--dry-run' || '' }}"
- run: "deno run --allow-net --allow-env --allow-run --allow-write shipit.ts ${{ github.ref_name != 'main' && '--dry-run' || '' }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ these are the most notable differences from upstream:
example.)
- [x] Unit tests for the more complex commit messages.
- [x] Update dependencies.
- [ ] TODO: Bump version number in any `deno.json` and `deno.jsonc`.
- [x] Write version number to any `deno.json` and `deno.jsonc`.
- [x] Dry-run mode, doesn't make any changes, but prints new version and release
notes.
- Use `--dry-run` or `-n` to enable, or set `DRY_RUN=1` in your environment.

## Usage

```sh
GITHUB_TOKEN="$(gh auth token)" deno run --allow-env --allow-run --allow-net https://raw.githubusercontent.com/hugojosefson/shipit/refs/heads/main/shipit.ts
GITHUB_TOKEN="$(gh auth token)" deno run --allow-env --allow-run --allow-net --allow-write https://raw.githubusercontent.com/hugojosefson/shipit/refs/heads/main/shipit.ts
```

If you'd prefer to run `@hugojosefson/shipit` on CI, you can:
Expand All @@ -77,7 +77,7 @@ on:
- uses: denoland/setup-deno@v2

# Use latest version of @hugojosefson/shipit. You may prefer to pin a specific version.
- run: deno run --allow-net --allow-env --allow-run https://raw.githubusercontent.com/hugojosefson/shipit/refs/heads/main/shipit.ts
- run: deno run --allow-env --allow-run --allow-net --allow-write https://raw.githubusercontent.com/hugojosefson/shipit/refs/heads/main/shipit.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
Expand All @@ -88,15 +88,16 @@ step.

## Granting permissions

`@hugojosefson/shipit` requires environment, network, and subprocess creation
permissions:
`@hugojosefson/shipit` requires environment, network, subprocess creation, and
write permissions:

- `--allow-env`: It reads `GITHUB_TOKEN` from your local environment in order to
authenticate with GitHub. It can also read `DRY_RUN` and `VERBOSE`.
- `--allow-run`: It needs to spawn subprocesses (`git`, `bash`) in order to
gather information about your commits.
- `--allow-net`: It needs to make outbound network requests to GitHub in order
to create GitHub releases.
- `--allow-write`: It needs to write to any `deno.json` and `deno.jsonc` files.

## Examples

Expand Down
4 changes: 3 additions & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@
"test": " deno test --coverage --trace-leaks --allow-run --allow-env=VERBOSE",
"test-watch": " deno test --watch --trace-leaks --allow-run --allow-env=VERBOSE",
"coverage": " deno coverage",
"release": " deno task all && GITHUB_TOKEN=$(gh auth token) deno run --allow-env=GITHUB_TOKEN,VERBOSE,DRY_RUN --allow-net=api.github.com --allow-run=bash,git shipit.ts",
"release": " deno task all && GITHUB_TOKEN=$(gh auth token) deno run --allow-env=GITHUB_TOKEN,VERBOSE,DRY_RUN --allow-net=api.github.com --allow-run=bash,git --allow-write=deno.json,deno.jsonc shipit.ts",
"bump-deps": " deno task forall-files-no-yaml -- deno run --allow-env --allow-read=.,$HOME/.cache/deno,$HOME/.local/share/deno-wasmbuild --allow-write=.,$HOME/.local/share/deno-wasmbuild --allow-run=git --allow-net jsr:@molt/cli@0.19.8 --commit --prefix=\"chore: \"",
"list-files": " git ls-files | deno eval 'import{toText}from\"jsr:@std/streams@1.0.8\";console.log((await toText(Deno.stdin.readable)).split(\"\\n\").filter(f=>f.startsWith(\".github/workflows\")||/\\.((mj|j|t)sx?|jsonc?)$/.test(f)).filter(f=>{try{return !Deno.statSync(f).isDirectory}catch{}}).join(\"\\n\"))'",
"foreach-file-no-json-yaml": "deno task list-files | grep -viE '\\.(jsonc?|ya?ml)$' | sh -c 'xargs -I {} -- \"$@\"'",
"forall-files-no-yaml": " deno task list-files | grep -viE '\\.ya?ml$' | sh -c 'xargs -d \"\\n\" -- \"$@\"'"
},
"imports": {
"@hugojosefson/fns": "jsr:@hugojosefson/fns@^2.1.0",
"@hugojosefson/run-simple": "jsr:@hugojosefson/run-simple@^2.3.8",
"@octokit/request": "npm:@octokit/request@^9.1.3",
"@std/assert": "jsr:@std/assert@^1.0.8",
"@std/fmt": "jsr:@std/fmt@^1.0.3",
"@std/semver": "jsr:@std/semver@^1.0.3",
"jsonc-parser": "npm:jsonc-parser@^3.3.1",
"parse-repo": "npm:parse-repo@^1.0.4"
}
}
19 changes: 18 additions & 1 deletion deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

125 changes: 125 additions & 0 deletions json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
applyEdits,
findNodeAtLocation,
getNodeValue,
type JSONPath,
type ModificationOptions,
modify,
type Node,
parseTree,
} from "jsonc-parser";

const MODIFICATION_OPTIONS: ModificationOptions = {
formattingOptions: {
insertSpaces: true,
tabSize: 2,
},
};

/**
* Set the value at the given JSON path in the given JSON or JSONC string.
* @param jsonc JSON or JSONC string to modify.
* @param jsonPath JSON path to the value to set.
* @param value Value to set.
* @returns New JSON or JSONC string with the value set.
*/
export function setValueInJson<V>(
jsonc: string,
jsonPath: JSONPath,
value: V,
): string {
const edits = modify(jsonc, jsonPath, value, MODIFICATION_OPTIONS);
return applyEdits(jsonc, edits);
}

/**
* Set the value at the given JSON path in the given JSON or JSONC file.
* @param filePath Path to the JSON or JSONC file to modify.
* @param jsonPath JSON path to the value to set.
* @param value Value to set.
* @returns Promise that resolves when the value is set.
*/
export async function setValueInJsonFile<V>(
filePath: string,
jsonPath: JSONPath,
value: V,
): Promise<void> {
const jsonc = await Deno.readTextFile(filePath);
const newJsonc = setValueInJson(jsonc, jsonPath, value);
await Deno.writeTextFile(filePath, newJsonc);
}

/**
* Get the value at the given JSON path in the given JSON or JSONC string.
* @param jsonc JSON or JSONC string to get the value from.
* @param jsonPath JSON path to the value to get.
* @param defaultValue Default value to return if the value is not found.
* @returns Value at the given JSON path in the given JSON or JSONC string.
*/
export function getValueInJson<V>(
jsonc: string,
jsonPath: JSONPath,
defaultValue: V | undefined = undefined,
): V | undefined {
const tree: Node | undefined = parseTree(jsonc);
if (tree === undefined) {
return defaultValue;
}
const value: Node | undefined = findNodeAtLocation(tree, jsonPath);
if (value === undefined) {
return defaultValue;
}
return getNodeValue(value);
}

/**
* Get the value at the given JSON path in the given JSON or JSONC file.
* @param filePath Path to the JSON or JSONC file to get the value from.
* @param jsonPath JSON path to the value to get.
* @param defaultValue Default value to return if the file or the value is not
* found.
* @returns Promise that resolves to the value at the given JSON path in the
* given JSON or JSONC file.
*/
export async function getValueInJsonFile<V>(
filePath: string,
jsonPath: JSONPath,
defaultValue: V | undefined = undefined,
): Promise<V | undefined> {
const jsonc = await Deno.readTextFile(filePath);
return getValueInJson(jsonc, jsonPath, defaultValue);
}

/**
* Get the version from the given `deno.json` or `deno.jsonc` file.
* @param filePath Path to the `deno.json` or `deno.jsonc` file to get the
* version from.
* @returns Promise that resolves to the version from the given `deno.json` or
* `deno.jsonc` file.
*/
export async function getVersionInDenoJsonFile<V>(
filePath: string,
): Promise<V | undefined> {
return await getValueInJsonFile<V>(
filePath,
["version"],
);
}

/**
* Set the version in the given `deno.json` or `deno.jsonc` file.
* @param filePath Path to the `deno.json` or `deno.jsonc` file to set the
* version in.
* @param version Version to set.
* @returns Promise that resolves when the version is set.
*/
export async function setVersionInDenoJsonFile(
filePath: string,
version: string,
): Promise<void> {
await setValueInJsonFile<string>(
filePath,
["version"],
version,
);
}
44 changes: 44 additions & 0 deletions shipit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import {
} from "./commit-regex.ts";
import * as colors from "@std/fmt/colors";
import * as semver from "@std/semver";
import { swallow } from "@hugojosefson/fns/fn/swallow";
import git, { ROOT } from "./git.ts";
import github, { type Commits, generateReleaseNotes } from "./github.ts";
import { setVersionInDenoJsonFile } from "./json.ts";
import { logHeader } from "./log.ts";
import { run } from "@hugojosefson/run-simple";

if (!import.meta.main) {
Deno.exit(0);
Expand Down Expand Up @@ -96,6 +99,47 @@ if (
Deno.exit(0);
}

logHeader("Updating version in any deno.json and deno.jsonc...");

// Sanity check: make sure there are still no uncommitted changes (there shouldn't be).
if (!(await git.isClean())) {
console.error(
`There are unexpectedly uncommitted changes in the middle of the release. Aborting.

This is likely a bug in @hugojosefson/shipit — My bad.

If you are able, please file a bug report at https://github.com/hugojosefson/shipit/issues
Include what you feel comfortable with, from the following:

- The exact shipit command you ran, that failed.
- The output from the shipit command you ran.
- The output from the following commands, after the error:
git status
git diff
git diff --cached
git log
git fetch # only relevant with the first "git fetch" after the error

Thank you!
`,
);
Deno.exit(1);
}

// Write the new version to any `deno.json` and `deno.jsonc`, if needed, and commit.
await setVersionInDenoJsonFile("deno.json", nextVer).catch(
swallow(Deno.errors.NotFound),
);
await setVersionInDenoJsonFile("deno.jsonc", nextVer).catch(
swallow(Deno.errors.NotFound),
);
if (!(await git.isClean())) {
console.log("Committing version change to deno.json*...");
await run(["git", "add", "."]);
await run(["git", "commit", "-m", `chore: release ${nextVer}`]);
console.log("Version change committed.");
}

// Tag the new version.
logHeader("Creating new remote tag...");
await git.tag(nextVer);
Expand Down