diff --git a/docs/deployment.md b/docs/deployment.md index aa8b36300..019445a3a 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -152,7 +152,13 @@ You can then deploy the site using: curvenote deploy ``` -:::{tip} Using GitHub Actions +You can also deploy from a GitHub action, which will build your site and then deploy it to Curvenote. + +🛠 In the root of your git repository run `myst init --gh-curvenote` + +The command will ask you questions about which branch to deploy from (e.g. `main`) and the name of the GitHub Action (e.g. `deploy.yml`). It will then create a GitHub Action[^actions] that will run next time you push your code to the main branch you specified. Ensure that you including setting up your `CURVENOTE_TOKEN` which can be created from your Curvenote profile. + +:::{tip} Full GitHub Actions :class: dropdown You can use GitHub actions to build and deploy your site automatically when you merge new documents, for example. @@ -167,13 +173,16 @@ on: branches: - main permissions: + contents: read pull-requests: write jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Deploy 🚀 - uses: curvenote/action-myst-publish + uses: curvenote/action-myst-publish@v1 + env: + CURVENOTE_TOKEN: ${{ secrets.CURVENOTE_TOKEN }} ``` ::: diff --git a/packages/myst-cli/src/build/gh-pages/index.ts b/packages/myst-cli/src/build/gh-actions/index.ts similarity index 59% rename from packages/myst-cli/src/build/gh-pages/index.ts rename to packages/myst-cli/src/build/gh-actions/index.ts index b8d316308..a579b7f29 100644 --- a/packages/myst-cli/src/build/gh-pages/index.ts +++ b/packages/myst-cli/src/build/gh-actions/index.ts @@ -65,6 +65,33 @@ jobs: `; } +function createGithubCurvenoteAction({ defaultBranch = 'main' }: { defaultBranch?: string }) { + return `# This file was created automatically with \`myst init --gh-curvenote\` 🪄 💚 + +name: Curvenote Deploy +on: + push: + # Runs on pushes targeting the default branch + branches: [${defaultBranch}] +permissions: + # Sets permissions of the GITHUB_TOKEN to allow read of private repos + contents: read +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: 'pages' + cancel-in-progress: false +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Deploy 🚀 + uses: curvenote/action-myst-publish@v1 + env: + CURVENOTE_TOKEN: \${{ secrets.CURVENOTE_TOKEN }} +`; +} + export async function getGithubUrl() { try { const gitUrl = await makeExecutable('git config --get remote.origin.url', null)(); @@ -96,7 +123,7 @@ async function checkAtGitRoot(): Promise { } } -export async function githubPagesAction(session: ISession) { +async function prelimGitChecks(session: ISession): Promise { const inGitRepo = await checkFolderIsGit(); if (!inGitRepo) { session.log.info( @@ -119,25 +146,34 @@ export async function githubPagesAction(session: ISession) { if (!githubUrl) { session.log.warn(`Could not read the GitHub URL from your git repository.`); } - session.log.info(`📝 Creating a GitHub Action to deploy your MyST Site\n`); - const prompt = await inquirer.prompt([ - { - name: 'branch', - message: `What branch would you like to deploy from?`, - default: 'main', - }, - { - name: 'name', - message: `What would you like to call the action?`, - default: 'deploy.yml', - validate(input: string) { - if (!input.endsWith('.yml')) return 'The GitHub Action name must end in `.yml`'; - const exists = fs.existsSync(path.join('.github', 'workflows', input)); - if (exists) return 'The workflow file already exists, please choose another name.'; - return true; - }, + return githubUrl; +} + +type Prompt = { branch: string; name: string }; + +const workflowQuestions = [ + { + name: 'branch', + message: `What branch would you like to deploy from?`, + default: 'main', + }, + { + name: 'name', + message: `What would you like to call the action?`, + default: 'deploy.yml', + validate(input: string) { + if (!input.endsWith('.yml')) return 'The GitHub Action name must end in `.yml`'; + const exists = fs.existsSync(path.join('.github', 'workflows', input)); + if (exists) return 'The workflow file already exists, please choose another name.'; + return true; }, - ]); + }, +]; + +export async function githubPagesAction(session: ISession) { + const githubUrl = await prelimGitChecks(session); + session.log.info(`📝 Creating a GitHub Action to deploy your MyST Site\n`); + const prompt = await inquirer.prompt(workflowQuestions); const [repo, org] = githubUrl ? githubUrl.split('/').reverse() : []; const action = createGithubPagesAction({ isGithubIO: githubUrl?.endsWith('.github.io'), @@ -166,6 +202,47 @@ ${filename} : 'on your https://{{ organization }}.github.io/{{ repo }} domain' } 7. 🎉 Celebrate and tell us about your site on Twitter or Mastodon! 🐦 🐘 - `, +`, + ); +} + +export async function githubCurvenoteAction(session: ISession) { + const githubUrl = await prelimGitChecks(session); + session.log.info(`📝 Creating a GitHub Action to deploy your site to Curvenote\n`); + const prompt = await inquirer.prompt(workflowQuestions); + const action = createGithubCurvenoteAction({ defaultBranch: prompt.branch }); + const filename = path.join('.github', 'workflows', prompt.name); + writeFileToFolder(filename, action); + session.log.info( + ` +🎉 GitHub Action is configured: + +${filename} + +✅ ${chalk.bold.green('Next Steps')} + +1. Ensure you have a domain set in your site configuration + + site: + domains: + - username.curve.space + +2. Create a new Curvenote API token + + https://curvenote.com/profile?settings=true&tab=profile-api + +3. Navigate to your GitHub settings for action secrets${ + githubUrl ? `\n\n ${githubUrl}/settings/secrets/actions\n` : '' + } +4. Add a new repository secret + + Name: ${chalk.blue.bold('CURVENOTE_TOKEN')} + Secret: Your Curvenote API Token + +5. Push these changes (and/or merge to ${prompt.branch}) +6. Look for a new action to start${githubUrl ? `\n\n ${githubUrl}/actions\n` : ''} +7. Once the action completes, your site should be deployed +8. 🎉 Celebrate and tell us about your site on Twitter or Mastodon! 🐦 🐘 +`, ); } diff --git a/packages/myst-cli/src/build/init.ts b/packages/myst-cli/src/build/init.ts index 58d04cfd1..dab47a997 100644 --- a/packages/myst-cli/src/build/init.ts +++ b/packages/myst-cli/src/build/init.ts @@ -8,7 +8,7 @@ import type { ISession } from '../session/index.js'; import inquirer from 'inquirer'; import chalk from 'chalk'; import { startServer } from './site/start.js'; -import { getGithubUrl, githubPagesAction } from './gh-pages/index.js'; +import { getGithubUrl, githubCurvenoteAction, githubPagesAction } from './gh-actions/index.js'; const VERSION_CONFIG = '# See docs at: https://mystmd.org/guide/frontmatter\nversion: 1\n'; @@ -38,6 +38,7 @@ export type InitOptions = { site?: boolean; writeToc?: boolean; ghPages?: boolean; + ghCurvenote?: boolean; }; const WELCOME = () => ` @@ -55,9 +56,10 @@ Learn more about this CLI and MyST Markdown at: ${chalk.bold('https://mystmd.org `; export async function init(session: ISession, opts: InitOptions) { - const { project, site, writeToc, ghPages } = opts; + const { project, site, writeToc, ghPages, ghCurvenote } = opts; if (ghPages) return githubPagesAction(session); + if (ghCurvenote) return githubCurvenoteAction(session); if (!project && !site && !writeToc) { session.log.info(WELCOME()); diff --git a/packages/mystmd/src/init.ts b/packages/mystmd/src/init.ts index 8acac1e18..8d2e48dee 100644 --- a/packages/mystmd/src/init.ts +++ b/packages/mystmd/src/init.ts @@ -3,10 +3,11 @@ import { Command } from 'commander'; import { Session, init } from 'myst-cli'; import { clirun } from './clirun.js'; import { - makeGithubActionOption, makeProjectOption, makeSiteOption, makeWriteTocOption, + makeGithubPagesOption, + makeGithubCurvenoteOption, } from './options.js'; export function makeInitCLI(program: Command) { @@ -15,7 +16,8 @@ export function makeInitCLI(program: Command) { .addOption(makeProjectOption('Initialize config for MyST project content')) .addOption(makeSiteOption('Initialize config for MyST site')) .addOption(makeWriteTocOption()) - .addOption(makeGithubActionOption()) + .addOption(makeGithubPagesOption()) + .addOption(makeGithubCurvenoteOption()) .action(clirun(Session, init, program)); return command; } diff --git a/packages/mystmd/src/options.ts b/packages/mystmd/src/options.ts index ba24d5c1e..bd3db0379 100644 --- a/packages/mystmd/src/options.ts +++ b/packages/mystmd/src/options.ts @@ -55,13 +55,20 @@ export function makeWriteTocOption() { ).default(false); } -export function makeGithubActionOption() { +export function makeGithubPagesOption() { return new Option( '--gh-pages', 'Creates a GitHub action that will deploy your site to GitHub pages', ).default(false); } +export function makeGithubCurvenoteOption() { + return new Option( + '--gh-curvenote', + 'Creates a GitHub action that will deploy your site to Curvenote', + ).default(false); +} + export function makeForceOption() { return new Option( '--force',