diff --git a/.github/scripts/update-homebrew/commit-push.sh b/.github/scripts/update-homebrew/commit-push.sh new file mode 100755 index 0000000..e099d98 --- /dev/null +++ b/.github/scripts/update-homebrew/commit-push.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Commit the updated formula as the fin-releases bot and push to homebrew-tap. +# Idempotent: exits 0 if the formula already matches. +# +# Required env: APP_SLUG, TOKEN, VERSION +# Assumes homebrew-tap has been checked out at ./homebrew-tap with TOKEN +# already configured as the remote credential. +set -euo pipefail + +BOT_NAME="${APP_SLUG}[bot]" +BOT_ID=$(GH_TOKEN="$TOKEN" gh api "/users/${BOT_NAME}" --jq .id) + +cd homebrew-tap +git config user.name "$BOT_NAME" +git config user.email "${BOT_ID}+${BOT_NAME}@users.noreply.github.com" + +git add Formula/fintoc.rb +if git diff --cached --quiet; then + echo "Formula already up to date" + exit 0 +fi + +git commit -m "fintoc $VERSION" -m "https://github.com/fintoc-com/fintoc-cli/releases/tag/v$VERSION" +git push diff --git a/.github/scripts/update-homebrew/download-tarball.sh b/.github/scripts/update-homebrew/download-tarball.sh new file mode 100755 index 0000000..b80f305 --- /dev/null +++ b/.github/scripts/update-homebrew/download-tarball.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Download the published tarball of @fintoc/cli@$VERSION and compute its +# SHA256. Writes `url` and `sha256` to $GITHUB_OUTPUT for downstream steps +# (the Homebrew formula needs both). +# +# Required env: VERSION, GITHUB_OUTPUT +set -euo pipefail + +URL="https://registry.npmjs.org/@fintoc/cli/-/cli-${VERSION}.tgz" +TARBALL=$(mktemp) + +HTTP_CODE=$(curl -sL -o "$TARBALL" -w '%{http_code}' "$URL") +if [ "$HTTP_CODE" != "200" ]; then + echo "Failed to download tarball (HTTP $HTTP_CODE)" + exit 1 +fi + +SHA256=$(sha256sum "$TARBALL" | awk '{print $1}') +echo "sha256=$SHA256" >> "$GITHUB_OUTPUT" +echo "url=$URL" >> "$GITHUB_OUTPUT" +echo "Tarball downloaded: $URL (sha256=$SHA256)" diff --git a/.github/scripts/update-homebrew/resolve-version.sh b/.github/scripts/update-homebrew/resolve-version.sh new file mode 100755 index 0000000..1ea7f30 --- /dev/null +++ b/.github/scripts/update-homebrew/resolve-version.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Resolve and validate the version to sync into the Homebrew formula. +# Strips a leading "v" and refuses anything not matching x.y.z. +# +# Required env: RAW_VERSION (may be empty if neither workflow_dispatch input +# nor release event provided one), GITHUB_OUTPUT +set -euo pipefail + +if [ -z "${RAW_VERSION:-}" ]; then + echo "::error::No version provided and no release tag in event" + exit 1 +fi + +VERSION="${RAW_VERSION#v}" +if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::Invalid version: $VERSION" + exit 1 +fi + +echo "version=$VERSION" >> "$GITHUB_OUTPUT" +echo "Resolved version: $VERSION" diff --git a/.github/scripts/update-homebrew/update-formula.sh b/.github/scripts/update-homebrew/update-formula.sh new file mode 100755 index 0000000..53f2151 --- /dev/null +++ b/.github/scripts/update-homebrew/update-formula.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Required env: TARBALL_URL, TARBALL_SHA256, VERSION +# Assumes homebrew-tap has been checked out at ./homebrew-tap. +set -euo pipefail + +for var in TARBALL_URL TARBALL_SHA256 VERSION; do + [ -z "${!var}" ] && echo "::error::$var is empty" && exit 1 +done + +envsubst '$TARBALL_URL $TARBALL_SHA256 $VERSION' > homebrew-tap/Formula/fintoc.rb <<'RUBY' +class Fintoc < Formula + desc "CLI for the Fintoc API" + homepage "https://github.com/fintoc-com/fintoc-cli" + url "$TARBALL_URL" + version "$VERSION" + sha256 "$TARBALL_SHA256" + license "BSD-3-Clause" + + depends_on "node" + + def install + system "npm", "install", *std_npm_args + bin.install_symlink Dir["#{libexec}/bin/*"] + end + + test do + assert_match "fintoc/#{version}", shell_output("#{bin}/fintoc --version") + end +end +RUBY + +echo "Formula written for version $VERSION" diff --git a/.github/scripts/update-homebrew/wait-npm.sh b/.github/scripts/update-homebrew/wait-npm.sh new file mode 100755 index 0000000..3541f66 --- /dev/null +++ b/.github/scripts/update-homebrew/wait-npm.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Poll the npm registry until @fintoc/cli@$VERSION shows up. +# Required env: VERSION +set -euo pipefail + +MAX_ATTEMPTS=30 +SLEEP_SECONDS=10 + +for i in $(seq 1 "$MAX_ATTEMPTS"); do + if npm view "@fintoc/cli@$VERSION" version 2>/dev/null; then + echo "Found @fintoc/cli@$VERSION on npm" + exit 0 + fi + echo "Attempt $i/$MAX_ATTEMPTS - waiting ${SLEEP_SECONDS}s..." + sleep "$SLEEP_SECONDS" +done + +echo "Timed out waiting for @fintoc/cli@$VERSION on npm" +npm view "@fintoc/cli" versions --json || true +exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bea7bc5..ec76dff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,166 +1,57 @@ name: Release on: - pull_request: - types: [closed] - branches: [main] - -permissions: - contents: read + workflow_dispatch: + inputs: + bump: + description: 'Tipo de bump' + type: choice + required: true + default: minor + options: [patch, minor, major] + release-notes: + description: 'Release notes (markdown, opcional)' + type: string + required: false jobs: - checks: - if: github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/') + release: + if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest - outputs: - version: ${{ steps.version.outputs.version }} - tag_name: ${{ steps.version.outputs.tag_name }} + permissions: + contents: write + id-token: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 + registry-url: https://registry.npmjs.org cache: npm - - name: Get version - id: version - run: | - VERSION=$(node -p 'require("./package.json").version') - if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then - echo "::error::Invalid version: $VERSION" - exit 1 - fi - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "tag_name=v$VERSION" >> "$GITHUB_OUTPUT" - - run: npm ci - run: npm run lint - run: npm run typecheck - run: npm run build - run: npm test - publish: - needs: checks - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 + - uses: fintoc-com/release-action/prepare@main + id: prep with: - node-version: 22 - registry-url: https://registry.npmjs.org - cache: npm + bump: ${{ inputs.bump }} + version-format: npm + tag-prefix: v + app-id: ${{ vars.FIN_RELEASES_APP_ID }} + app-private-key: ${{ secrets.FIN_RELEASES_PRIVATE_KEY }} - - run: npm ci - run: npm publish --access public --provenance env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - update-homebrew: - needs: [checks, publish] - runs-on: ubuntu-latest - steps: - - name: Wait for npm publish - run: | - VERSION="${{ needs.checks.outputs.version }}" - for i in $(seq 1 30); do - if npm view "@fintoc/cli@$VERSION" version 2>/dev/null; then - echo "Found @fintoc/cli@$VERSION on npm" - exit 0 - fi - echo "Attempt $i/30 - waiting 10s..." - sleep 10 - done - echo "Timed out waiting for @fintoc/cli@$VERSION on npm" - npm view "@fintoc/cli" versions --json || true - exit 1 - - - name: Download tarball and compute SHA256 - id: sha - run: | - VERSION="${{ needs.checks.outputs.version }}" - URL="https://registry.npmjs.org/@fintoc/cli/-/cli-${VERSION}.tgz" - TARBALL=$(mktemp) - HTTP_CODE=$(curl -sL -o "$TARBALL" -w '%{http_code}' "$URL") - if [ "$HTTP_CODE" != "200" ]; then - echo "Failed to download tarball (HTTP $HTTP_CODE)" - exit 1 - fi - SHA256=$(sha256sum "$TARBALL" | awk '{print $1}') - echo "sha256=$SHA256" >> "$GITHUB_OUTPUT" - echo "url=$URL" >> "$GITHUB_OUTPUT" - - - name: Clone homebrew-tap - uses: actions/checkout@v4 + - uses: fintoc-com/release-action/finalize@main with: - repository: fintoc-com/homebrew-tap - token: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} - path: homebrew-tap - - - name: Update formula - env: - TARBALL_URL: ${{ steps.sha.outputs.url }} - TARBALL_SHA256: ${{ steps.sha.outputs.sha256 }} - VERSION: ${{ needs.checks.outputs.version }} - run: | - for var in TARBALL_URL TARBALL_SHA256 VERSION; do - [ -z "${!var}" ] && echo "::error::$var is empty" && exit 1 - done - envsubst '$TARBALL_URL $TARBALL_SHA256 $VERSION' > homebrew-tap/Formula/fintoc.rb <<'RUBY' - class Fintoc < Formula - desc "CLI for the Fintoc API" - homepage "https://github.com/fintoc-com/fintoc-cli" - url "$TARBALL_URL" - version "$VERSION" - sha256 "$TARBALL_SHA256" - license "BSD-3-Clause" - - depends_on "node" - - def install - system "npm", "install", *std_npm_args - bin.install_symlink Dir["#{libexec}/bin/*"] - end - - test do - assert_match "fintoc/#{version}", shell_output("#{bin}/fintoc --version") - end - end - RUBY - - - name: Commit and push - run: | - VERSION="${{ needs.checks.outputs.version }}" - cd homebrew-tap - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add Formula/fintoc.rb - if git diff --cached --quiet; then - echo "Formula already up to date" - exit 0 - fi - git commit -m "fintoc $VERSION" -m "https://github.com/fintoc-com/fintoc-cli/releases/tag/v$VERSION" - git push - - tag: - needs: [checks, publish] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - - name: Create and push tag - env: - TAG_NAME: ${{ needs.checks.outputs.tag_name }} - run: | - if git ls-remote --exit-code --tags origin "refs/tags/$TAG_NAME" >/dev/null 2>&1; then - echo "::error::Tag $TAG_NAME already exists" - exit 1 - fi - git tag "$TAG_NAME" - git push origin "$TAG_NAME" + tag: ${{ steps.prep.outputs.tag }} + release-notes: ${{ inputs.release-notes }} + app-id: ${{ vars.FIN_RELEASES_APP_ID }} + app-private-key: ${{ secrets.FIN_RELEASES_PRIVATE_KEY }} diff --git a/.github/workflows/update-homebrew.yml b/.github/workflows/update-homebrew.yml new file mode 100644 index 0000000..688690b --- /dev/null +++ b/.github/workflows/update-homebrew.yml @@ -0,0 +1,64 @@ +name: Update Homebrew tap + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: 'Versión a sincronizar (default: último release publicado)' + required: false + type: string + +jobs: + update: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Resolve version + id: ver + env: + RAW_VERSION: ${{ inputs.version || github.event.release.tag_name }} + run: ./.github/scripts/update-homebrew/resolve-version.sh + + - name: Wait for npm publish + env: + VERSION: ${{ steps.ver.outputs.version }} + run: ./.github/scripts/update-homebrew/wait-npm.sh + + - name: Download tarball and compute SHA256 + id: sha + env: + VERSION: ${{ steps.ver.outputs.version }} + run: ./.github/scripts/update-homebrew/download-tarball.sh + + - name: Generate App token for homebrew-tap + id: tap-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.FIN_RELEASES_APP_ID }} + private-key: ${{ secrets.FIN_RELEASES_PRIVATE_KEY }} + owner: fintoc-com + repositories: homebrew-tap + + - name: Clone homebrew-tap + uses: actions/checkout@v4 + with: + repository: fintoc-com/homebrew-tap + token: ${{ steps.tap-token.outputs.token }} + path: homebrew-tap + + - name: Update formula + env: + TARBALL_URL: ${{ steps.sha.outputs.url }} + TARBALL_SHA256: ${{ steps.sha.outputs.sha256 }} + VERSION: ${{ steps.ver.outputs.version }} + run: ./.github/scripts/update-homebrew/update-formula.sh + + - name: Commit and push + env: + APP_SLUG: ${{ steps.tap-token.outputs.app-slug }} + TOKEN: ${{ steps.tap-token.outputs.token }} + VERSION: ${{ steps.ver.outputs.version }} + run: ./.github/scripts/update-homebrew/commit-push.sh diff --git a/RELEASE.md b/RELEASE.md index 51f4166..9f51eef 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,32 +1,30 @@ -# Release +# Releasing -## How to publish a new version +Manual desde Actions. Toma ~3–5 min. -1. Create a release branch and bump the version: +## Cómo -```bash -git checkout main && git pull -git checkout -b release/vX.X.X -npm version patch --no-git-tag-version # or minor/major -``` +1. https://github.com/fintoc-com/fintoc-cli/actions → **Release** → **Run workflow**. +2. `bump`: `patch` / `minor` / `major`. `release-notes`: markdown opcional. +3. **Run workflow**. Solo corre desde `main`. -2. Commit, push, and create a PR: +## Qué hace -```bash -git add package.json package-lock.json -git commit -m "chore: bump version to X.X.X" -git push -u origin release/vX.X.X -gh pr create --title "Version X.X.X 🎉" -``` +`Release` workflow (`release.yml`): -3. Merge the PR. Everything else is automatic — `release.yml` detects the merged `release/*` branch, creates a `vX.X.X` tag, and publishes to npm. +`npm ci` + `lint` + `typecheck` + `build` + `test` → +[`release/prepare`](https://github.com/fintoc-com/release-action/tree/main/prepare) bumpea local → +`npm publish --access public --provenance` → +[`release/finalize`](https://github.com/fintoc-com/release-action/tree/main/finalize) pushea commit + tag, crea GitHub Release. -4. Verify: +Eso dispara automáticamente `Update Homebrew tap` (`update-homebrew.yml`): -```bash -npm view @fintoc/cli version -``` +Espera propagación de npm → descarga tarball, calcula SHA → actualiza `Formula/fintoc.rb` en [`homebrew-tap`](https://github.com/fintoc-com/homebrew-tap) → push como `fin-releases[bot]`. -## CI requirements +## Si falla -The publish workflow requires a `NPM_TOKEN` secret in the repo (Settings → Secrets and variables → Actions). This is a Granular Access Token from npmjs.com with read/write permissions on `@fintoc/cli` and 2FA bypass enabled. +| Falla en | Estado | Recovery | +|---|---|---| +| Antes o durante `npm publish` | Nada en el remote | Re-run del Release workflow | +| `release/finalize` (post-publish) | Paquete en npm, sin tag/release | PR con el commit del bump + `gh release create` | +| `Update Homebrew tap` | npm + tag + release OK, fórmula atrasada | Re-run del Update Homebrew (acepta `version` input para una versión específica) |