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
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,23 @@ jobs:
- run: bun run lint
- run: bun run format:check

version-sync:
name: Version sync
if: >-
always()
&& (github.event_name != 'pull_request'
|| needs.trust-check.outputs.trusted == 'true')
needs: [trust-check]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "22.21.1"
- run: node scripts/version-sync.mjs check

test:
name: Test
if: >-
Expand Down
158 changes: 69 additions & 89 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,59 +1,67 @@
name: Release

env:
NODE_VERSION: "22"
NPM_VERSION: "11.11.1"

on:
push:
branches: [main]
paths: [VERSION]
workflow_dispatch:
inputs:
tag:
description: Existing git tag to release, e.g. v0.0.1
required: true
type: string
publish_to_npm:
description: Publish the package to npm after creating the GitHub release
description: Publish the current VERSION to npm after verification
Comment thread
jan-kubica marked this conversation as resolved.
required: true
default: false
Comment thread
jan-kubica marked this conversation as resolved.
type: boolean

concurrency:
group: release-${{ inputs.tag }}
group: release-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
release-ref:
name: Release ref
if: >-
github.event_name == 'workflow_dispatch'
&& inputs.publish_to_npm
&& github.ref != 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- run: |
echo "::error::Manual npm publishing must run from main."
exit 1

preflight:
name: Preflight
if: >-
github.event_name != 'workflow_dispatch'
|| inputs.publish_to_npm != true
|| github.ref == 'refs/heads/main'
uses: stella/.github/.github/workflows/npm-version-preflight.yml@1372ecc2728373e324db00eb0c99825a852e60a9
with:
package-files: package.json
publish-to-npm: >-
${{ github.event_name == 'push'
|| (inputs.publish_to_npm && github.ref == 'refs/heads/main') }}

verify:
name: Verify release inputs
name: Verify
needs: preflight
if: needs.preflight.outputs.already-released != 'true'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: ${{ env.NODE_VERSION }}

node-version: "22.21.1"
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2

- name: Validate tag and package version
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
git rev-parse --verify "refs/tags/${RELEASE_TAG}" >/dev/null

PKG_VERSION="$(node -p "require('./package.json').version")"
TAG_VERSION="${RELEASE_TAG#v}"
if [[ "$PKG_VERSION" != "$TAG_VERSION" ]]; then
echo "::error::Tag version (${TAG_VERSION}) does not match package.json version (${PKG_VERSION})."
exit 1
fi

- run: bun install --frozen-lockfile
- run: bun run version:check
- run: bun run lint
- run: bun test
- run: bun run typecheck
Expand All @@ -62,82 +70,54 @@ jobs:
- run: ORACLE_SAMPLES=500 bun run oracle
- run: npm pack --json --ignore-scripts --dry-run

publish:
name: Publish
needs: [verify]
pack:
name: Pack
needs: [preflight, verify]
if: needs.preflight.outputs.already-released != 'true'
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: refs/tags/${{ inputs.tag }}

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: https://registry.npmjs.org

- name: Install npm for release publishing
run: npm install --global "npm@${{ env.NPM_VERSION }}"

node-version: "22.21.1"
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- run: bun install --frozen-lockfile
- run: bun run build

- name: Pack release tarball
id: pack
shell: bash
run: |
set -euo pipefail

mkdir -p release-artifacts

pack_json="$(npm pack --json --ignore-scripts --pack-destination release-artifacts)"
tarball_file="$(echo "$pack_json" | jq -r '.[0].filename')"
sha256sum "release-artifacts/$tarball_file" > release-artifacts/SHA256SUMS

echo "tarball=release-artifacts/$tarball_file" >> "$GITHUB_OUTPUT"

- name: Create or update GitHub release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail

assets=(
"${{ steps.pack.outputs.tarball }}"
release-artifacts/SHA256SUMS
npm pack --json --ignore-scripts --pack-destination release-artifacts
(
cd release-artifacts
sha256sum -- ./*.tgz > SHA256SUMS
)

prerelease_flag=()
if [[ "$RELEASE_TAG" == *-* ]]; then
prerelease_flag+=(--prerelease)
fi

if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber
else
gh release create "$RELEASE_TAG" "${assets[@]}" \
"${prerelease_flag[@]}" \
--title "$RELEASE_TAG"
fi

- name: Publish to npm
if: inputs.publish_to_npm
uses: stella/.github/.github/actions/npm-publish-hardened@96b8912e0786548c4e62046ce3965769991dd0e0
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
tarball: ${{ steps.pack.outputs.tarball }}

update-changelog:
name: Update CHANGELOG
needs: publish
if: inputs.publish_to_npm
uses: stella/.github/.github/workflows/changelog-update.yml@314d81ed84537155fb17d57ebdf227aeaca4f907
name: release-artifacts
path: release-artifacts
if-no-files-found: error

finalize:
name: Finalize
needs: [preflight, pack]
if: >-
needs.preflight.outputs.already-released != 'true'
&& (github.event_name == 'push'
|| (inputs.publish_to_npm && github.ref == 'refs/heads/main'))
uses: stella/.github/.github/workflows/npm-version-finalize.yml@1372ecc2728373e324db00eb0c99825a852e60a9
with:
tag: ${{ inputs.tag }}
package-files: package.json
publish-to-npm: >-
${{ github.event_name == 'push'
|| (inputs.publish_to_npm && github.ref == 'refs/heads/main') }}
permissions:
contents: write
id-token: write
Comment thread
jan-kubica marked this conversation as resolved.
pull-requests: write
secrets: inherit
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.0.0
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,9 @@
"format:check": "oxfmt --check . \"!.ai/**\" \"!.agents/**\" \"!.claude/**\" \"!AGENTS.md\" \"!CLAUDE.md\" \"!GEMINI.md\"",
"typecheck": "tsc -p tsconfig.json",
"sync-ai:check": "bash scripts/sync-ai-skills.sh --check",
"lint:fix": "bun --bun oxlint -c oxlint.config.ts --type-aware --fix ."
"lint:fix": "bun --bun oxlint -c oxlint.config.ts --type-aware --fix .",
"version:check": "node scripts/version-sync.mjs check",
"version:sync": "node scripts/version-sync.mjs sync"
},
"devDependencies": {
"@brazilian-utils/brazilian-utils": "^2.3.0",
Expand Down
121 changes: 121 additions & 0 deletions scripts/version-sync.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env node

import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

const ROOT = fileURLToPath(new URL("../", import.meta.url));
const VERSION_PATTERN =
/^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:alpha|beta|rc)\.[0-9]+)?$/;

const repoPath = (...segments) =>
path.join(ROOT, ...segments);

const readJson = (filePath) =>
JSON.parse(fs.readFileSync(filePath, "utf8"));

const writeJson = (filePath, value) => {
fs.writeFileSync(
filePath,
`${JSON.stringify(value, null, 2)}\n`,
);
};

const readVersion = () =>
fs.readFileSync(repoPath("VERSION"), "utf8").trim();

const writeVersion = (version) => {
fs.writeFileSync(repoPath("VERSION"), `${version}\n`);
};

const parseArgs = () => {
const [command, ...rest] = process.argv.slice(2);
const args = new Map();

for (let index = 0; index < rest.length; index += 1) {
const token = rest[index];
if (token === "--version" || token === "--tag") {
const value = rest[index + 1];
if (value == null) {
throw new Error(`Missing value for ${token}`);
}
args.set(token.slice(2), value);
index += 1;
continue;
}
throw new Error(`Unknown argument: ${token}`);
}

return { command, args };
};

const expectedVersion = (args) => {
const tag = args.get("tag");
const version =
args.get("version") ??
(tag ? tag.replace(/^v/, "") : readVersion());
if (!VERSION_PATTERN.test(version)) {
throw new Error(
`Expected 1.2.3, 1.2.3-rc.1, 1.2.3-beta.1, or 1.2.3-alpha.1; got '${version}'`,
);
}
return version;
};

const syncVersion = (version) => {
const packageJsonPath = repoPath("package.json");
const manifest = readJson(packageJsonPath);
writeVersion(version);
manifest.version = version;
writeJson(packageJsonPath, manifest);
};

const checkVersion = (version) => {
const packageJsonPath = repoPath("package.json");
const manifest = readJson(packageJsonPath);
const mismatches = [];

if (readVersion() !== version) {
mismatches.push(
`${repoPath("VERSION")}: expected ${version}`,
);
}
if (manifest.version !== version) {
mismatches.push(
`${packageJsonPath}: version=${manifest.version}; expected ${version}`,
);
}

if (mismatches.length === 0) {
return;
}

console.error("Version drift detected:");
for (const mismatch of mismatches) {
console.error(`- ${mismatch}`);
}
process.exit(1);
};

const main = () => {
const { command, args } = parseArgs();

if (command !== "sync" && command !== "check") {
console.error(
"Usage: node scripts/version-sync.mjs <sync|check> [--version <semver>] [--tag <git-tag>]",
);
process.exit(1);
}

const version = expectedVersion(args);

if (command === "sync") {
syncVersion(version);
return;
}

checkVersion(version);
};
Comment thread
jan-kubica marked this conversation as resolved.

main();
Loading