Skip to content
Merged
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
21 changes: 21 additions & 0 deletions RELEASE_WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,27 @@ When release-please opens or updates the release pull request:

</details>

## Security Releases

Security fixes must be published as normal GitHub releases, with the release title or notes clearly including
`Security`, a `CVE-*`, or a `GHSA-*` identifier when one applies. The optional operator upgrade nudge uses those markers
to make behind-version notices louder when the release gap includes a security fix.

When a vulnerability affects released vanityURLs code, also publish a GitHub Security Advisory so operators using
GitHub's `Watch -> Releases` and security notification workflows receive the strongest available platform signal.

<details>
<summary>Security release checklist</summary>

- Confirm the fix is merged through the normal reviewed release flow.
- Confirm the release notes identify the security impact without exposing unnecessary exploit detail before operators
can patch.
- Include `Security`, `CVE-*`, or `GHSA-*` in the GitHub release title or body.
- Publish or update the matching GitHub Security Advisory when appropriate.
- Push only the signed release tag after local verification.

</details>

## Signed Release Tag

Configure gitsign before creating release tags:
Expand Down
85 changes: 85 additions & 0 deletions defaults/github/workflows/vanityurls-upgrade-nudge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: vanityURLs upgrade nudge

on:
schedule:
- cron: "17 13 1 * *"
workflow_dispatch:

permissions:
contents: read
issues: write

jobs:
check-upstream-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 24
- id: release
run: |
node scripts/check-upstream-release.mjs --json > release-status.json
{
echo "status<<EOF"
cat release-status.json
echo "EOF"
} >> "$GITHUB_OUTPUT"
- uses: actions/github-script@v8
env:
RELEASE_STATUS: ${{ steps.release.outputs.status }}
with:
script: |
const status = JSON.parse(process.env.RELEASE_STATUS);
if (!status.ok || !status.behind) {
core.info(`No upgrade issue needed: ${status.status}`);
return;
}

const owner = context.repo.owner;
const repo = context.repo.repo;
const marker = "<!-- vanityurls-upgrade-nudge -->";
const title = status.security
? `Security update available: vanityURLs ${status.latestVersion}`
: `vanityURLs ${status.latestVersion} is available`;
const body = [
marker,
"",
status.security ? "## Security update available" : "## Update available",
"",
`This instance appears to be running vanityURLs ${status.currentVersion}.`,
`The latest upstream release is ${status.latestVersion}.`,
"",
`- Upstream release: ${status.latestUrl}`,
`- Releases behind: ${status.behindCount}`,
`- Security-related releases in the gap: ${status.securityCount}`,
"",
"This is a pull-based, privacy-preserving nudge from this repository's own scheduled workflow.",
"Review the release, run the upgrade workflow documented for this instance, and close this issue when done."
].join("\n");

const { data: issues } = await github.rest.issues.listForRepo({
owner,
repo,
state: "open",
per_page: 100
});
const existing = issues.find((issue) => !issue.pull_request && issue.body && issue.body.includes(marker));

if (existing) {
await github.rest.issues.update({
owner,
repo,
issue_number: existing.number,
title,
body
});
return;
}

await github.rest.issues.create({
owner,
repo,
title,
body
});
22 changes: 22 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,35 @@ npm run test
npm run validate
npm run smoke
npm run local-install
node scripts/check-upstream-release.mjs
./scripts/v8s-lnk --help
./scripts/v8s-lnk list
```

Grouped commands run the whole group by default. Use focused variants such as `npm run test:worker`,
`npm run check:links`, `npm run validate:targets`, or `npm run smoke:analytics` when you only need one layer.

## Optional upgrade nudge

vanityURLs does not phone home. To get a pull-based monthly reminder when this instance falls behind upstream releases,
copy the workflow template into this repository:

```bash
mkdir -p .github/workflows
cp defaults/github/workflows/vanityurls-upgrade-nudge.yml .github/workflows/
```

The workflow checks the public GitHub releases API monthly and opens or updates one issue in this repository when a
newer vanityURLs release is available. It does not send this instance's links or configuration upstream.

For an opt-in local check, run:

```bash
npm run doctor -- --check-upstream
```

Offline or unavailable network checks are non-fatal.

## Documentation

Use the vanityURLs documentation site for setup, customization, and operations:
Expand Down
62 changes: 62 additions & 0 deletions scripts/check-upstream-release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env node

import { checkUpstreamRelease, currentPackageVersion, formatUpstreamReleaseNotice } from "./lib/upstream-release.mjs";

function parseArgs(argv) {
const args = {
currentVersion: "",
json: false,
repository: "vanityURLs/code"
};

for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--json") {
args.json = true;
} else if (arg === "--current-version") {
args.currentVersion = readValue(argv, ++index, arg);
} else if (arg === "--repo") {
args.repository = readValue(argv, ++index, arg);
} else if (arg === "--help" || arg === "-h") {
printHelp();
process.exit(0);
} else {
throw new Error(`Unknown argument: ${arg}`);
}
}

return args;
}

function readValue(argv, index, flag) {
const value = argv[index];
if (!value || value.startsWith("--")) throw new Error(`Missing value for ${flag}`);
return value;
}

function printHelp() {
console.log(`Usage: node scripts/check-upstream-release.mjs [options]

Options:
--json Print machine-readable JSON
--current-version <version> Override the local package version
--repo <owner/name> Upstream repository (default: vanityURLs/code)
`);
}

try {
const args = parseArgs(process.argv.slice(2));
const result = await checkUpstreamRelease({
currentVersion: args.currentVersion || currentPackageVersion(),
repository: args.repository
});

if (args.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(formatUpstreamReleaseNotice(result));
}
} catch (error) {
console.error(`[release-check] ${error.message}`);
process.exitCode = 1;
}
30 changes: 20 additions & 10 deletions scripts/doctor.mjs
Original file line number Diff line number Diff line change
@@ -1,40 +1,50 @@
#!/usr/bin/env node

import { diagnoseCustomPublic, loadMaintenanceContext } from "./lib/custom-public-maintenance.mjs";
import { checkUpstreamRelease, formatUpstreamReleaseNotice } from "./lib/upstream-release.mjs";

function parseArgs(argv) {
const args = { json: false };
const args = { checkUpstream: process.env.V8S_CHECK_UPSTREAM_RELEASE === "1", json: false };
for (const arg of argv) {
if (arg === "--json") {
args.json = true;
} else if (arg === "--check-upstream") {
args.checkUpstream = true;
} else {
throw new Error(`Unknown argument: ${arg}`);
}
}
return args;
}

function main() {
async function main() {
const args = parseArgs(process.argv.slice(2));
const context = loadMaintenanceContext();
const issues = diagnoseCustomPublic(context);
const upstreamRelease = args.checkUpstream ? await checkUpstreamRelease() : null;

if (args.json) {
console.log(JSON.stringify({ issues }, null, 2));
const payload = { issues };
if (args.checkUpstream) payload.upstream_release = upstreamRelease;
console.log(JSON.stringify(payload, null, 2));
return;
}

if (!issues.length) {
console.log("[doctor] No custom public drift detected.");
return;
}
} else {
console.log(`[doctor] Found ${issues.length} custom public issue${issues.length === 1 ? "" : "s"}:`);
for (const issue of issues) {
console.log(`- [${issue.severity}] ${issue.path}: ${issue.message}`);
}

console.log(`[doctor] Found ${issues.length} custom public issue${issues.length === 1 ? "" : "s"}:`);
for (const issue of issues) {
console.log(`- [${issue.severity}] ${issue.path}: ${issue.message}`);
printRecommendedFixes(issues);
}

printRecommendedFixes(issues);
if (upstreamRelease) {
console.log("");
console.log(formatUpstreamReleaseNotice(upstreamRelease));
}
}

function printRecommendedFixes(issues) {
Expand All @@ -61,7 +71,7 @@ function compareFixes(left, right) {
}

try {
main();
await main();
} catch (error) {
console.error(`[doctor] ${error.message}`);
process.exitCode = 1;
Expand Down
2 changes: 2 additions & 0 deletions scripts/help.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const sections = [
["npm run setup", "Configure or refresh instance-owned settings."],
["npm run detach", "Detach a clone from the upstream product repository."],
["npm run upgrade", "Refresh product-owned files while preserving custom/."],
["npm run doctor -- --check-upstream", "Opt into a non-fatal upstream release check."],
["node scripts/check-upstream-release.mjs", "Check the latest upstream release manually."],
["npm run local-install", "Install workstation helper commands."],
["npm run local-publish", "Run local checks, select commits, and push local changes."],
["npm run generate:blocklist", "Generate blocklist feed data."]
Expand Down
Loading
Loading