diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 089553e..4d6341c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: CI on: pull_request: + workflow_call: permissions: contents: read @@ -30,6 +31,8 @@ jobs: run: cargo install taplo-cli --version 0.10.0 --locked - name: Check TOML formatting and alphabetical key order run: taplo fmt --check + - name: Validate Cargo manifest schema + run: taplo lint Cargo.toml check: name: Check, Clippy, tests, and binary (${{ matrix.os }}) @@ -62,8 +65,19 @@ jobs: run: cargo build --locked --release - name: Smoke test run: ./target/release/filetrail --help + - name: Verify crates.io package without publishing + run: cargo publish --dry-run --locked --registry crates-io + - name: Package standalone binary + shell: bash + run: | + version=$(cargo metadata --locked --no-deps --format-version 1 | python3 -c 'import json, sys; print(json.load(sys.stdin)["packages"][0]["version"])') + target=$(rustc -vV | sed -n 's/^host: //p') + package="filetrail-v${version}-${target}" + mkdir -p "target/dist/$package" target/artifacts + cp target/release/filetrail LICENSE README.md README_zh.md "target/dist/$package/" + tar -czf "target/artifacts/$package.tar.gz" -C target/dist "$package" - uses: actions/upload-artifact@v4 with: name: filetrail-${{ runner.os }}-${{ runner.arch }} - path: target/release/filetrail + path: target/artifacts/*.tar.gz if-no-files-found: error diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..c528075 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,101 @@ +name: Publish + +on: + push: + tags: ['v*'] + +permissions: + contents: read + +concurrency: + group: publish-${{ github.ref }} + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + ci: + name: Release checks + uses: ./.github/workflows/ci.yml + + publish: + name: Publish to crates.io + needs: ci + runs-on: ubuntu-latest + environment: crates-io + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Install pinned toolchain + run: rustup show + - name: Check tag matches Cargo.toml version + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + python3 - <<'PY' + import os + import tomllib + + with open("Cargo.toml", "rb") as manifest: + version = tomllib.load(manifest)["package"]["version"] + expected = f"v{version}" + actual = os.environ["RELEASE_TAG"] + if actual != expected: + raise SystemExit(f"Tag {actual!r} does not match Cargo.toml version: expected {expected!r}") + print(f"Publishing filetrail {version} from {actual}") + PY + - name: Publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + if [ -z "$CARGO_REGISTRY_TOKEN" ]; then + echo '::error::Set CARGO_REGISTRY_TOKEN in the crates-io GitHub environment.' + exit 1 + fi + cargo publish --locked --registry crates-io + + release: + name: Prepare draft GitHub Release + needs: publish + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + pattern: filetrail-* + merge-multiple: true + path: artifacts + - name: Generate and verify SHA-256 checksums + working-directory: artifacts + shell: bash + run: | + shopt -s nullglob + linux_archives=(filetrail-*-unknown-linux-gnu.tar.gz) + macos_archives=(filetrail-*-apple-darwin.tar.gz) + if (( ${#linux_archives[@]} == 0 || ${#macos_archives[@]} == 0 )); then + echo '::error::Both Linux and macOS release archives are required.' + exit 1 + fi + sha256sum -- filetrail-*.tar.gz > SHA256SUMS + sha256sum --check SHA256SUMS + - name: Create or update draft release + working-directory: artifacts + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.ref_name }} + run: | + if gh release view "$RELEASE_TAG" --json isDraft --jq '.isDraft' > "$RUNNER_TEMP/filetrail-release-draft"; then + if [ "$(cat "$RUNNER_TEMP/filetrail-release-draft")" != true ]; then + echo '::error::The release is already published; refusing to change it.' + exit 1 + fi + gh release upload "$RELEASE_TAG" filetrail-*.tar.gz SHA256SUMS --clobber + else + gh release create "$RELEASE_TAG" filetrail-*.tar.gz SHA256SUMS \ + --draft --verify-tag --title "FileTrail $RELEASE_TAG" --generate-notes + fi diff --git a/.schemas/cargo.json b/.schemas/cargo.json new file mode 100644 index 0000000..2285fa1 --- /dev/null +++ b/.schemas/cargo.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$comment": "Taplo 0.10.0 does not load external references hidden inside anyOf. Validate Cargo lints explicitly so its schema is loaded before validating the complete manifest. Both validations use the official SchemaStore schemas.", + "allOf": [ + { + "properties": { + "lints": { + "properties": { + "cargo": { + "$ref": "https://www.schemastore.org/cargo-lints-cargo.json" + } + } + } + } + }, + { + "$ref": "https://www.schemastore.org/cargo.json" + } + ] +} diff --git a/.taplo.toml b/.taplo.toml index a492c70..08a28cd 100644 --- a/.taplo.toml +++ b/.taplo.toml @@ -5,3 +5,9 @@ include = ["**/*.toml"] column_width = 100 reorder_inline_tables = true reorder_keys = true + +[[rule]] +include = ["Cargo.toml"] + +[rule.schema] +path = ".schemas/cargo.json" diff --git a/AGENTS.md b/AGENTS.md index 075d586..a6c5d77 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,9 @@ one executable for macOS and Linux. Read README.md before changing its behavior. `cargo test --locked --all-targets`, and `cargo test --locked --doc`. - Completion integration tests require Bash, Zsh, and Fish on PATH. - Run `taplo fmt` and `taplo fmt --check` with taplo-cli 0.10.0. +- Run `taplo lint Cargo.toml` to validate the manifest schema. `.taplo.toml` selects + `.schemas/cargo.json`, which works around Taplo's loading of external references + inside `anyOf` while preserving the complete official Cargo schema validation. - Every Rust import must be its own `use` statement. Do not use grouped braces. rustfmt's `imports_granularity = "Item"` enforces this on the pinned nightly. - Keep TOML keys alphabetically sorted, particularly `[package]` and dependency @@ -24,6 +27,39 @@ one executable for macOS and Linux. Read README.md before changing its behavior. - Keep OS service installation out of automated tests. Test rendered definitions. - Do not introduce a dependency on an external `git` executable for core commands. +## Publishing to crates.io + +- PR CI runs `cargo publish --dry-run --locked --registry crates-io` on macOS and + Linux without credentials. The final `aborting upload due to dry run` warning is + Cargo's expected confirmation that nothing was uploaded. +- `.github/workflows/publish.yml` runs on pushed `v*` tags and reuses the complete + CI workflow. Only after all checks pass does it require an exact + `v` match (for example, `v0.1.0`), then publish to crates.io. +- After crates.io publishing succeeds, the workflow creates a draft GitHub + Release with generated notes. It attaches the tested macOS and Linux binaries + as `filetrail-v-.tar.gz` archives, including both READMEs + and the license, plus a `SHA256SUMS` file. Archives preserve executable permissions. + Publish the GitHub Release manually after reviewing it. A rerun can replace + assets on an existing draft but refuses to modify a published release. If this + job fails after crates.io publishing, rerun only failed jobs to avoid publishing + the same crate version again. +- In GitHub repository Settings → Environments, create `crates-io`, allow release + tags matching `v*`, and add an environment secret named `CARGO_REGISTRY_TOKEN`. + Its value must be a crates.io API token, not a GitHub personal access token. + No extra GitHub credentials are needed. Only the draft Release job grants the + built-in `GITHUB_TOKEN` `contents: write`; other jobs use `contents: read`. +- The crates.io account must have a verified email and permission to publish + `filetrail`. The token needs permission to publish new crates for the first + release, and publish new versions for later releases. Scope it to `filetrail` + where supported and set an expiration date. Create tokens at + . +- Update `Cargo.toml` and the root package version in `Cargo.lock` together, + commit the release changes, then push the matching tag. For version `0.1.0`: + `git tag v0.1.0` followed by `git push origin v0.1.0`. + A published version cannot be overwritten; each new release needs a new version. +- For local verification of uncommitted changes only, add `--allow-dirty` to the + dry-run command. CI and actual publishing deliberately require a clean checkout. + ## Architecture and invariants Core modules: `config.rs` manages mappings, `state.rs` persists sync state, diff --git a/Cargo.toml b/Cargo.toml index 6861818..abd2f90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,9 @@ [package] description = "Watch files, sync them into a Git repository, and commit on your terms." edition = "2024" +license = "MIT" name = "filetrail" +repository = "https://github.com/RinChanNOWWW/FileTrail" version = "0.1.0" [dependencies] @@ -22,6 +24,10 @@ tempfile = "3.20" toml = "0.9" walkdir = "2.5" +[lints.cargo] +# Cargo explicitly inserts readme into the packaged manifest on the pinned nightly. +manual_readme = "allow" + [profile.release] codegen-units = 1 lto = "thin"