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
131 changes: 131 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: Release

# Tag-triggered release for the copilot.py uv script. Publishes copilot.py as a
# release asset (verbatim — its sha256 is just the file's hash) and regenerates
# the Homebrew formula in natikgadzhi/homebrew-taps so `brew upgrade` picks it up.
#
# Why workflow_dispatch in addition to `on: push tags`: a tag pushed with the
# default GITHUB_TOKEN does NOT start other workflows, so tag.yml dispatches this
# explicitly on the new tag ref (same pattern as the copilot-auth repo).
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: "Tag to release (e.g. v0.4.0)"
required: true

permissions:
contents: write # create the Release + upload assets

jobs:
release:
name: Publish asset and update the tap
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Resolve version
id: version
run: |
set -euo pipefail
ref="${{ github.event.inputs.tag || github.ref_name }}"
tag="${ref#refs/tags/}"
version="${tag#v}"
if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Could not parse a semver from tag '${tag}'." >&2
exit 1
fi
# The asset is copilot.py verbatim, so __version__ must match the tag —
# otherwise the published file would disagree with the release version.
script_version="$(grep -E '^__version__ = ' copilot.py | sed -E 's/.*"([^"]+)".*/\1/')"
if [[ "${script_version}" != "${version}" ]]; then
echo "__version__ (${script_version}) does not match tag (${version})." >&2
exit 1
fi
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "version=${version}" >> "$GITHUB_OUTPUT"

- name: Compute SHA-256
id: checksum
run: |
set -euo pipefail
sha="$(sha256sum copilot.py | awk '{print $1}')"
echo "sha256=${sha}" >> "$GITHUB_OUTPUT"
echo "copilot.py sha256=${sha}"

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag }}
files: copilot.py
body: |
Install with Homebrew:

```sh
brew install natikgadzhi/taps/copilot-cli
copilot-cli --version
```

**copilot.py sha256:** `${{ steps.checksum.outputs.sha256 }}`

- name: Check out the Homebrew tap
uses: actions/checkout@v4
with:
repository: natikgadzhi/homebrew-taps
token: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
path: tap

- name: Regenerate the formula
env:
VERSION: ${{ steps.version.outputs.version }}
SHA256: ${{ steps.checksum.outputs.sha256 }}
run: |
set -euo pipefail
cat > tap/Formula/copilot-cli.rb <<EOF
# typed: false
# frozen_string_literal: true

# This file is updated by the copilot-python release workflow. DO NOT EDIT.
class CopilotCli < Formula
desc "Personal CLI for Copilot Money: sync to SQLite, edit transactions, export"
homepage "https://github.com/natikgadzhi/copilot-python"
url "https://github.com/natikgadzhi/copilot-python/releases/download/v${VERSION}/copilot.py"
sha256 "${SHA256}"
version "${VERSION}"
license "MIT"

# copilot.py is a single PEP 723 uv script: deps are declared inline and
# resolved (and cached) by uv on first run, so there is no virtualenv to
# manage here — we just need uv on the box.
depends_on "uv"

def install
libexec.install "copilot.py"
(bin/"copilot-cli").write <<~SH
#!/bin/bash
exec "#{Formula["uv"].opt_bin}/uv" run --script "#{libexec}/copilot.py" "\$@"
SH
chmod 0755, bin/"copilot-cli"
end

test do
assert_match version.to_s, shell_output("#{bin}/copilot-cli --version")
end
end
EOF

- name: Commit and push the formula
run: |
set -euo pipefail
cd tap
if git diff --quiet -- Formula/copilot-cli.rb; then
echo "Formula already up to date."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Formula/copilot-cli.rb
git commit -m "Brew formula update for copilot-cli version v${{ steps.version.outputs.version }}"
git push origin HEAD:main
78 changes: 78 additions & 0 deletions .github/workflows/tag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Tag release

# Manual version bump + tag. Pick a bump level; this bumps __version__ in
# copilot.py (the single source of truth), commits "Release vX.Y.Z" to main,
# creates the matching tag, and kicks off the release build.
#
# Why dispatch release.yml explicitly instead of letting the tag push trigger
# it: a tag pushed with the default GITHUB_TOKEN does NOT start other workflows
# (GitHub blocks recursive triggers), so `on: push tags` in release.yml would
# never fire. We push the tag, then dispatch release.yml on that tag ref.
on:
workflow_dispatch:
inputs:
bump:
description: "Version bump level"
type: choice
required: true
default: patch
options:
- patch
- minor
- major

permissions:
contents: write # push the bump commit + tag
actions: write # dispatch release.yml

jobs:
tag:
name: Bump version and tag
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0

- name: Compute next version
id: version
run: |
set -euo pipefail
current="$(grep -E '^__version__ = ' copilot.py | sed -E 's/.*"([0-9]+\.[0-9]+\.[0-9]+)".*/\1/')"
if [[ ! "${current}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Could not parse __version__ from copilot.py (got '${current}')." >&2
exit 1
fi
IFS=. read -r major minor patch <<< "${current}"
case "${{ inputs.bump }}" in
major) major=$((major + 1)); minor=0; patch=0 ;;
minor) minor=$((minor + 1)); patch=0 ;;
patch) patch=$((patch + 1)) ;;
esac
next="${major}.${minor}.${patch}"
if git rev-parse "v${next}" >/dev/null 2>&1; then
echo "Tag v${next} already exists." >&2
exit 1
fi
echo "next=${next}" >> "$GITHUB_OUTPUT"
echo "Bumping ${current} -> ${next}"

- name: Bump, commit, and tag
env:
NEXT: ${{ steps.version.outputs.next }}
run: |
set -euo pipefail
sed -i -E "s/(^__version__ = \")[0-9]+\.[0-9]+\.[0-9]+(\")/\1${NEXT}\2/" copilot.py
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "Release v${NEXT}"
git tag -a "v${NEXT}" -m "Release v${NEXT}"
git push origin HEAD:main
git push origin "v${NEXT}"

- name: Trigger release for the new tag
env:
GH_TOKEN: ${{ github.token }}
NEXT: ${{ steps.version.outputs.next }}
run: gh workflow run release.yml --field tag="v${NEXT}"
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,28 @@ to manage) with these subcommands:
| `export` | Read the SQLite DB and emit `accounts.{csv,md}` + `categories.{csv,md}`. |
| `stats` | Print row counts, the latest transaction, and the last sync time. |

## Install

Install with Homebrew — this pulls in `uv` and puts a `copilot-cli` command on
your PATH, so you never have to type `uv run copilot.py` again:

```sh
brew install natikgadzhi/taps/copilot-cli
copilot-cli --version
```

`copilot-cli` is a thin wrapper around the same `copilot.py` uv script (deps are
still resolved and cached by uv on first run), so it's a drop-in replacement for
`uv run copilot.py` everywhere below: `copilot-cli sync`, `copilot-cli stats`,
and so on.

Prefer to run from a clone? Install [uv](https://docs.astral.sh/uv/getting-started/installation/)
and use `uv run copilot.py …` directly — no install step needed.

## Setup

1. Install [uv](https://docs.astral.sh/uv/getting-started/installation/).
1. Install [uv](https://docs.astral.sh/uv/getting-started/installation/) (or
`brew install natikgadzhi/taps/copilot-cli`, which bundles it).
2. Provide two secrets — the Firebase `FIREBASE_API_KEY` and
`COPILOT_REFRESH_TOKEN` — by **either** of:

Expand Down
Loading