ocre-cli 0.2.0

Command-line tool for Ocre: create, generate, migrate, run and deploy apps.
//! `ocre generate ci`: `.github/workflows/ci.yml`, the GitHub Actions
//! workflow of the app. A `check` job runs the steps of `ocre ci` (format,
//! clippy, tests, the wasm32 build, translations when there are locales) on
//! every push and pull request; a `deploy` job runs `ocre deploy` after it
//! on pushes to `main`, authenticated by the `CLOUDFLARE_API_TOKEN` and
//! `CLOUDFLARE_ACCOUNT_ID` repository secrets.

use std::fmt::Write as _;

use super::Edits;
use crate::{
    CliResult,
    ci::{I18N_STEP, STEPS},
    project::Project,
};

/// The workflow, relative to the app root.
pub const WORKFLOW: &str = ".github/workflows/ci.yml";

/// How CI installs the `ocre` CLI (as the README says).
const INSTALL_OCRE: &str = "cargo install --git https://github.com/tgeselle/ocre.rs ocre-cli";

const HEADER: &str = r#"# CI for this Ocre app. Generated by `ocre g ci`; `ocre ci` runs the same
# checks locally, in the same order.
#
# The deploy job needs two repository secrets (Settings > Secrets and
# variables > Actions):
#   CLOUDFLARE_API_TOKEN   an API token (My Profile > API Tokens) with Account
#                          permissions "Workers Scripts: Edit", "D1: Edit", and
#                          when the app uses them "Queues: Edit", "Workers KV
#                          Storage: Edit", "Workers R2 Storage: Edit"; plus
#                          "Zone: Workers Routes: Edit" for custom domains.
#   CLOUDFLARE_ACCOUNT_ID  the account id (Workers & Pages overview), needed
#                          when the token can see several accounts.
# Commit package-lock.json (`npm ci` installs the pinned cf and wrangler) and
# the KV ids the first `ocre deploy` writes into cloudflare.config.ts.
name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # rust-toolchain.toml adds the wasm32-unknown-unknown target.
      - run: rustup component add rustfmt clippy
      - uses: Swatinem/rust-cache@v2
"#;

pub fn ci(project: &Project) -> CliResult {
    let mut workflow = HEADER.to_owned();
    for (step, _) in STEPS {
        writeln!(workflow, "      - run: {step}").expect("writing to a String");
    }
    write!(
        workflow,
        r#"      # Translations: every key of the default locale in every locale.
      - if: hashFiles('locales/*.yml') != ''
        run: {INSTALL_OCRE} && {I18N_STEP}

  deploy:
    needs: check
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    # One deploy at a time; a newer push waits for the running one.
    concurrency: deploy
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - uses: Swatinem/rust-cache@v2
      - run: npm ci
      - run: {INSTALL_OCRE}
      # Builds the Worker, creates missing resources, applies remote migrations.
      - run: ocre deploy --json
        env:
          CLOUDFLARE_API_TOKEN: ${{{{ secrets.CLOUDFLARE_API_TOKEN }}}}
          CLOUDFLARE_ACCOUNT_ID: ${{{{ secrets.CLOUDFLARE_ACCOUNT_ID }}}}
          CF_SEND_TELEMETRY: "false"
"#
    )
    .expect("writing to a String");
    let mut edits = Edits::new(project);
    edits.create(WORKFLOW, workflow)?;
    let mut report = edits.apply("generate ci")?;
    report.next = vec![
        "add the repository secrets CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID (see the comment at the top of the workflow)"
            .to_owned(),
        "ocre ci (the same checks, locally)".to_owned(),
        "git add .github package-lock.json && git commit, then push to main".to_owned(),
    ];
    Ok(report)
}