diff --git a/.claude/skills/add-doc-page/SKILL.md b/.claude/skills/add-doc-page/SKILL.md new file mode 100644 index 0000000..2263304 --- /dev/null +++ b/.claude/skills/add-doc-page/SKILL.md @@ -0,0 +1,107 @@ +--- +name: add-doc-page +description: Add a new documentation page to this repo and register it so it + renders on amezmo.com. Use when creating any new .md page, including API + endpoint pages. Covers the section index entry, reciprocal backlinks, the + nav registration in the dashboard app, and the lint checks. +--- + +# Add a Documentation Page + +Adding a page is a two-repo change. The Markdown lives here, but the page +only renders on amezmo.com after it is registered in the dashboard app at +`~/Source/dashboard.amezmo.com`. A page that exists only in this repo is +unreachable: the menu, the prev/next nav, the sitemap, and the JSON-LD dates +all come from the dashboard's hand-maintained tree, not from file discovery. + +## 1. Write the Page + +Follow the conventions in [CLAUDE.md](../../../CLAUDE.md): a single `#` H1, +no front matter, relative `.md`-suffixed internal links, GitHub alert +callouts. API endpoint pages follow the fixed layout in the "API reference +pages" section of CLAUDE.md. + +An optional lead paragraph directly under the H1 can carry `{.lead}` on its +own line above the paragraph (see `cron/schedules.md`). The site styles it +as the page intro. + +## 2. Link It from Its Section Index + +Pages are not auto-discovered. Add a `###` entry for the page under the +section index's `## In This Section` run, with a one-or-two sentence excerpt +written from the page's content. The heading text is plain, never a link; +the link goes in the excerpt prose. Keep the entries in reading order: this +order should match the menu order you register in step 4. + +For top-level sections, check whether the root `index.md` or +`how-to-guides/index.md` should also point at the page. + +## 3. Make Content Links Reciprocal + +Every content page you link from the new page should link back with a +contextual link. Index pages are exempt. Record deliberate one-ways in +`.github/backlink-ignore.txt` as `SRC -> DST` with a comment. + +## 4. Register the Page in the Dashboard App + +Edit `app/Http/Docs/Services/DocumentationMenuService.php` in +`~/Source/dashboard.amezmo.com`. Find the section's +`DocumentationCategory` and add a child entry in the same position the page +has in the section index (menu order is also prev/next reading order). + +The constructor is: + +```php +// app/Http/Docs/Models/DocumentationCategory.php +public function __construct( + string $name, // menu label; also the slug source + ?string $description, // meta description for the page + ?string $parent, // parent category name, e.g. 'Cron' + ?array $children, // [] for a leaf page + bool $lineBreak, // false for leaf pages + $slug, // only when the filename differs from the name + ?string $title // override when it differs from name +) +``` + +The slug defaults to `Str::slug(strtolower($name))`, so `'Schedules'` +resolves to `schedules`. Pass an explicit slug when the filename does not +match the name (`'Create a deployment'` renders `post.md`, so it passes +`'post'`). The slug plus the parent chain must equal the file path in this +repo without `.md`: the `Cron` category's child with slug `schedules` renders +`cron/schedules.md`. + +Write the description with care: it becomes the page's meta description, +and the search engine snippet. + +## 5. Page Dates + +`app/Http/Docs/Generated/DocsPageDates.php` maps each page to its git +commit dates for the Article JSON-LD. It is generated by +`php artisan sitemap:generate` through the regenerate-sitemap workflow. Do +not edit it by hand. `DocsPageDatesTest` fails the dashboard CI when a menu +page has no dates, so the docs commit needs to land and the sitemap needs +regenerating after the menu entry is added. + +## 6. Validate + +Run from this repo's root: + +```bash +$ python3 .github/scripts/check_links.py +$ python3 .github/scripts/check_backlinks.py +$ python3 .github/scripts/check_image_paths.py +$ codespell --config .codespellrc +``` + +## Checklist + +- [ ] Page written per CLAUDE.md conventions +- [ ] `###` excerpt entry in the section's `index.md` +- [ ] Root `index.md` / `how-to-guides/index.md` updated if relevant +- [ ] Backlinks reciprocal, or the one-way recorded in + `.github/backlink-ignore.txt` +- [ ] `DocumentationCategory` entry added in `DocumentationMenuService.php`, + in the same order as the section index +- [ ] Sitemap regenerated after both changes land (page dates) +- [ ] Lint scripts pass diff --git a/.github/backlink-ignore.txt b/.github/backlink-ignore.txt index 01a549b..6ba3660 100644 --- a/.github/backlink-ignore.txt +++ b/.github/backlink-ignore.txt @@ -9,9 +9,18 @@ /docs/api/changelog -> /docs/api/deployments/post /docs/api/changelog -> /docs/api/environments/get-environment /docs/api/changelog -> /docs/api/environments/update-environment +/docs/api/changelog -> /docs/api/cron/create-cron-entry +/docs/api/changelog -> /docs/api/cron/get-cron-entry +/docs/api/changelog -> /docs/api/cron/list-cron-entries +/docs/api/changelog -> /docs/api/cron/update-cron-entry +/docs/api/changelog -> /docs/api/errors # API endpoints cross-link the lookup endpoints they depend on (see the API -# convention in CLAUDE.md); those lookups don't link back to every caller. +# convention in CLAUDE.md); those lookups don't link back to every caller. The +# errors reference is cited the same way: by everything, citing nothing back. +/docs/api/endpoints -> /docs/api/errors +/docs/api/cron/create-cron-entry -> /docs/api/errors +/docs/api/cron/update-cron-entry -> /docs/api/errors /docs/api/environments/update-environment -> /docs/api/environments/list-environments /docs/api/environments/update-environment -> /docs/api/instances/list-instance-types /docs/api/instances/create-instance -> /docs/api/instances/list-instance-types @@ -40,6 +49,7 @@ /docs/billing/payment-methods -> /docs/billing/advanced-pay /docs/php/versions -> /docs/how-to-guides/changing-php-versions /docs/php/composer -> /docs/instances/ssh +/docs/cron/custom-expressions -> /docs/instances/ssh # Nginx location-blocks references the domain routing/rules and http-auth pages; # those describe separate features and don't need to point back. @@ -65,6 +75,7 @@ /docs/configuration/logging -> /docs/deployments/directories /docs/npm/package-caching -> /docs/deployments/directories /docs/workers/reloading -> /docs/deployments/directories +/docs/cron/editing-entries -> /docs/deployments/directories /docs/deployments/directories -> /docs/deployments/hooks/deploy-success /docs/configuration/dotenv -> /docs/deployments/releases /docs/deployments/hooks/before-pull -> /docs/deployments/releases diff --git a/.github/scripts/check_backlinks.py b/.github/scripts/check_backlinks.py index 7b773cf..a592b81 100644 --- a/.github/scripts/check_backlinks.py +++ b/.github/scripts/check_backlinks.py @@ -13,7 +13,7 @@ Excluded: - index/landing pages (index.md) -- their nav is one-directional by design - - non-published files (CLAUDE.md, anything under .github/) + - non-published files (CLAUDE.md, README.md, anything under .github/) - pairs listed in .github/backlink-ignore.txt (known-intentional one-ways) Exit code is always 0. @@ -28,6 +28,11 @@ EXTERNAL = ("http://", "https://", "mailto:", "//") +# Repo meta files, not published doc pages: skipped as both link sources +# and link targets. +META_FILES = {"CLAUDE.md", "README.md"} + + def doc_files(root): files = set() for dirpath, _dirs, names in os.walk(root): @@ -35,7 +40,7 @@ def doc_files(root): if ".git" in parts or ".github" in parts: continue for name in names: - if name.endswith(".md") and name != "CLAUDE.md": + if name.endswith(".md") and name not in META_FILES: files.add(os.path.relpath(os.path.join(dirpath, name), root)) return files diff --git a/.github/scripts/check_links.py b/.github/scripts/check_links.py index df10878..ced84c7 100644 --- a/.github/scripts/check_links.py +++ b/.github/scripts/check_links.py @@ -22,6 +22,16 @@ EXTERNAL = ("http://", "https://", "mailto:", "//") +# Repo meta files, not published doc pages: excluded as link sources and +# link targets for the doc-page check. +META_FILES = {"CLAUDE.md", "README.md"} + +# Meta files whose own outgoing links are still checked. They may point at +# any existing repo file, not only published .md pages. CLAUDE.md is not +# here: its links are illustrative examples, not real paths. +CHECKED_META_FILES = {"README.md"} + + def doc_files(root): files = set() for dirpath, _dirs, names in os.walk(root): @@ -29,7 +39,7 @@ def doc_files(root): if ".git" in parts or ".github" in parts: continue for name in names: - if name.endswith(".md") and name != "CLAUDE.md": + if name.endswith(".md") and name not in META_FILES: files.add(os.path.relpath(os.path.join(dirpath, name), root)) return files @@ -53,6 +63,21 @@ def main(): if not path.endswith(".md") or target not in files: broken.append((rel, lineno, url)) + for rel in sorted(CHECKED_META_FILES): + full = os.path.join(ROOT, rel) + if not os.path.isfile(full): + continue + with open(full, encoding="utf-8") as handle: + for lineno, line in enumerate(handle, 1): + for match in LINK_RE.finditer(line): + url = match.group(1) + if not is_internal(url): + continue + path = url.split("#", 1)[0] + target = os.path.normpath(os.path.join(os.path.dirname(rel), path)) + if path and not os.path.isfile(os.path.join(ROOT, target)): + broken.append((rel, lineno, url)) + if broken: print(f"Found {len(broken)} broken internal link(s):") for rel, lineno, url in broken: diff --git a/README.md b/README.md new file mode 100644 index 0000000..3df629b --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# Amezmo Documentation + +[![Docs lint](https://github.com/amezmo/docs/actions/workflows/docs-lint.yml/badge.svg)](https://github.com/amezmo/docs/actions/workflows/docs-lint.yml) +[![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](https://github.com/amezmo/docs/pulls) + +The source for the [Amezmo](https://www.amezmo.com) docs: everything on +zero-downtime PHP hosting, from your first `git push` to tuning Nginx, OPcache, +and Redis on a production instance. Published at +[amezmo.com/docs](https://www.amezmo.com/docs/). + +**This repo is Markdown and nothing else.** No framework, no package manager, +no build step, no dependencies to install. Clone it, open a `.md` file in your +editor's preview, and you are looking at the page. That means the distance +between "this paragraph is wrong" and a merged fix is about four minutes. + +## Contribute + +You already know something we got wrong. Ship it: + +```bash +$ git clone git@github.com:amezmo/docs.git +$ cd docs +$ git checkout -b fix-cron-timezone-note +$ $EDITOR cron/index.md +$ python3 .github/scripts/check_links.py +``` + +Then open a pull request. Typo fixes, a missing flag, a code sample that no +longer runs, an entire new how-to guide: all of it is welcome, and small PRs +get merged fast. + +Good first contributions: + +- Fix a code sample you tried and found stale. +- Add the gotcha that cost you an hour, as a `> [!WARNING]` callout. +- Write the how-to guide for your framework in + [how-to-guides/](how-to-guides/index.md). +- Document an endpoint the [REST API](api/index.md) section is missing. + +Read the +[contributor rulebook](CLAUDE.md) +before a larger change. It covers page layout, the API reference format, the +changelog format, and the house writing style. It's written for Claude Code, and +it's the same rulebook a human needs. + +## Local Checks + +CI runs these four on every push and pull request. Run them from the repo root +first: + +```bash +$ python3 .github/scripts/check_links.py # internal links resolve +$ python3 .github/scripts/check_image_paths.py # images use absolute URLs +$ codespell --config .codespellrc # spelling +$ python3 .github/scripts/check_backlinks.py # backlinks (warn-only) +``` + +The first three gate the build. The backlink check only reports. + +## Conventions Worth Knowing Up Front + +Internal links are relative and keep their `.md` suffix, so they work in the +GitHub file browser and on the rendered site: `releases.md` for a sibling, +`../instances/scaling.md` across sections. Never use a `/docs/`-absolute or +extensionless path. + +Links are reciprocal. When one content page links to another, the target links +back. Landing pages are exempt, and deliberate one-way links are recorded in +[`.github/backlink-ignore.txt`](.github/backlink-ignore.txt) +with a comment explaining why. + +Pages render through league/commonmark with the attributes and description list +extensions, plus GitHub alert callouts: + +```markdown +> [!WARNING] +> Restoring a backup overwrites the target database. +``` + +New pages are linked from their section's `index.md`. Nothing is +auto-discovered by a nav generator. + +## The Map + +[index.md](index.md) is the home page. The big sections: + +| Section | What's in it | +| --- | --- | +| [Instances](instances/index.md) | Dedicated and full-stack servers, scaling, private networking | +| [Deployments](deployments/index.md) | Atomic zero-downtime releases, hooks, instant rollbacks | +| [Databases](databases/index.md) | Managed MySQL, backups, restores between environments | +| [REST API](api/index.md) | Endpoint reference for automating the dashboard | +| [How-to guides](how-to-guides/index.md) | Laravel, Craft CMS, Drupal, and friends | +| [Domains](domains/index.md), [Cron](cron/index.md), [Workers](workers/index.md) | The rest of the platform surface | + +## Contributors + +Built by the people who use it. + +[![Contributors](https://contrib.rocks/image?repo=amezmo/docs)](https://github.com/amezmo/docs/graphs/contributors) + +Your avatar goes here on your first merged PR. + +## See Also + +- [Amezmo on YouTube](https://www.youtube.com/@AmezmoPHP) +- [PHP hosting guides](https://www.amezmo.com/guides) +- [Amezmo on GitHub](https://www.github.com/amezmo) +- [Slack community](https://www.amezmo.com/goslack) diff --git a/api/changelog.md b/api/changelog.md index 424cf5a..0d951a1 100644 --- a/api/changelog.md +++ b/api/changelog.md @@ -2,6 +2,46 @@ The changelog lists forward-compatible changes to the API. +## 2026-09-12 + +Added +: [Create a cron entry](cron/create-cron-entry.md) creates an entry on an +instance: a permanent name, a bash script, a schedule, and the environment to +run it in. Deleting an entry stays in the dashboard. + +## 2026-09-10 + +Breaking +: `type` on an error response is now the coarse class of the error +(`invalid_request_error`, `authentication_error`, `rate_limit_error`, +`api_error`) rather than the specific reason. The specific reason moved to the +new `code` field, keeping the strings it used before, so a client matching on +`unknown_resource_error` or `validation_error` should read `code` instead of +`type`. See [Errors](errors.md). + +Added +: `code` to every error response: the specific, machine-readable reason, where +`type` is the coarse class. See [Errors](errors.md). + +Fixed +: Errors that carry their own response body (no payment method, unverified +email, an instance that is paused or still launching, a feature your instance +type does not include) returned a generic `500` with no usable message. They now +return their real status, message, and `code`. + +Fixed +: Error messages written for users were replaced with "An unexpected API error +occurred." outside development. The real message is now returned, and only +genuinely unexpected failures fall back to a generic one. + +Added +: [Cron endpoints](cron/index.md). [List cron entries](cron/list-cron-entries.md) +lists every entry on an instance, and +[Get a cron entry](cron/get-cron-entry.md) gets one by ID. +: [Update a cron entry](cron/update-cron-entry.md) updates the `command`, the +`expression`, or both, on an existing entry. An entry's `name` stays fixed, +because Amezmo derives its log file name from it. + ## 2022-12-12 Added diff --git a/api/cron/create-cron-entry.md b/api/cron/create-cron-entry.md new file mode 100644 index 0000000..e251599 --- /dev/null +++ b/api/cron/create-cron-entry.md @@ -0,0 +1,85 @@ +# Create a cron entry + +{.lead} +Create a **cron entry** on an instance: a name, a bash script, and the +schedule to run it on. + +`POST` /v1/instances/{instance_id}/cron + +## Parameters + +Parameter | Type | In | Required | Description +----------- | ------ | ---- | -------- | ------------------------------------------------------- +instance_id | string | uri | Yes | The instance ID +name | string | body | Yes | A permanent name for the entry, unique in its environment +command | string | body | Yes | The bash script to run +expression | string | body | Yes | The schedule +environment | string | body | Yes | The environment the entry runs in: `production` or `staging` + +`name` accepts letters, numbers, and hyphens, starts with a letter or a +number, and is at most 63 characters. Pick it carefully: Amezmo derives the +entry's log file name from it, so the name is fixed after creation. +[Update a cron entry](update-cron-entry.md) updates the script and the +schedule, never the name. + +Use `--data-urlencode` rather than `--data` for both `command` and +`expression`. A script contains newlines and an expression contains spaces and +`*`, and only the encoding form sends those intact. + +`expression` accepts a five-field POSIX cron expression or one of the +[schedule aliases](../../cron/schedules.md), such as `@minutely` or +`@daily`. Not every expression the API accepts runs on a Linux instance: see +[custom cron expressions](../../cron/custom-expressions.md) for the extensions +to avoid. + +The response reports the resolved five-field `expression` along with the +matching `expression_alias`, so an alias you send comes out as both. + +Amezmo applies the entry on your instance asynchronously: it writes the script +and the crontab entry, and runs begin at the next time the schedule matches. +The response carries the entry immediately, including the `id` you use with +[Get a cron entry](get-cron-entry.md) and +[Update a cron entry](update-cron-entry.md). + +A name already taken in the same environment returns `409 Conflict` with the +code `cron_entry_exists`. An environment your instance does not have returns +`422` with `business_logic_error`. An instance whose type does not include +cron returns `422` with `feature_not_supported`. See [Errors](../errors.md) +for the shape of all three. + +Delete a cron entry from the dashboard, under **Cron** on your instance. + +## Code samples + +### Request example + +{title="POST /v1/instances/{instance_id}/cron"} +```bash +curl https://api.amezmo.com/v1/instances/{instance_id}/cron \ + -X POST \ + -H "Authorization: Bearer $AMEZMO_API_KEY" \ + --data-urlencode name='Laravel-task-scheduler' \ + --data-urlencode command="#!/bin/bash +php artisan schedule:run +" \ + --data-urlencode expression='@minutely' \ + --data-urlencode environment='production' +``` + +### Response + +{title="201 Created"} +```javascript +{ + "id": 412, + "name": "Laravel-task-scheduler", + "command": "#!/bin/bash\nphp artisan schedule:run", + "expression": "* * * * *", + "expression_alias": "@minutely", + "status": "Created", + "environment_name": "production", + "log_file_path": "/home/deployer/cron/logs/cron.Laravel-task-scheduler.log", + "created_at": "2026-02-11T18:04:22.000000Z", + "updated_at": "2026-02-11T18:04:22.000000Z" +} +``` diff --git a/api/cron/get-cron-entry.md b/api/cron/get-cron-entry.md new file mode 100644 index 0000000..4859599 --- /dev/null +++ b/api/cron/get-cron-entry.md @@ -0,0 +1,46 @@ +# Get a cron entry + +{.lead} +Get a single **cron entry** by ID, including its schedule and its script. + +`GET` /v1/instances/{instance_id}/cron/{cron_id} + +## Parameters + +Parameter | Type | In | Required | Description +----------- | ------ | --- | -------- | ------------------------------------------------------- +instance_id | string | uri | Yes | The instance ID +cron_id | string | uri | Yes | The cron entry ID. See [List cron entries](list-cron-entries.md) + +The response carries the entry's full `command`, so this is how you read a +script before updating it with [Update a cron entry](update-cron-entry.md). A +freshly created entry needs no follow-up request: the response of +[Create a cron entry](create-cron-entry.md) carries the same fields. + +## Code samples + +### Request example + +{title="GET /v1/instances/{instance_id}/cron/{cron_id}"} +```bash +curl https://api.amezmo.com/v1/instances/{instance_id}/cron/412 \ + -H "Authorization: Bearer $AMEZMO_API_KEY" +``` + +### Response + +{title="200 OK"} +```javascript +{ + "id": 412, + "name": "Laravel-task-scheduler", + "command": "#!/bin/bash\nphp artisan schedule:run", + "expression": "* * * * *", + "expression_alias": "@minutely", + "status": "Created", + "environment_name": "production", + "log_file_path": "/home/deployer/cron/logs/cron.Laravel-task-scheduler.log", + "created_at": "2026-02-11T18:04:22.000000Z", + "updated_at": "2026-09-10T14:31:07.000000Z" +} +``` diff --git a/api/cron/index.md b/api/cron/index.md new file mode 100644 index 0000000..59bde2c --- /dev/null +++ b/api/cron/index.md @@ -0,0 +1,12 @@ +# Cron + +[Cron entries](../../cron/index.md) run a bash script on your instance on a +schedule. The API lists, gets, creates, and updates them. The following +endpoints are supported: + +- `GET` [List cron entries](list-cron-entries.md) +- `GET` [Get a cron entry](get-cron-entry.md) +- `POST` [Create a cron entry](create-cron-entry.md) +- `PATCH` [Update a cron entry](update-cron-entry.md) + +Delete a cron entry from the dashboard, under **Cron** on your instance. diff --git a/api/cron/list-cron-entries.md b/api/cron/list-cron-entries.md new file mode 100644 index 0000000..8476dd3 --- /dev/null +++ b/api/cron/list-cron-entries.md @@ -0,0 +1,47 @@ +# List cron entries + +{.lead} +List every **cron entry** on an instance, across all of its environments. + +`GET` /v1/instances/{instance_id}/cron + +## Parameters + +Parameter | Type | In | Required | Description +----------- | ------ | --- | -------- | --------------- +instance_id | string | uri | Yes | The instance ID + +Each entry reports the environment it belongs to in `environment_name`, so one +call covers both staging and production. To get a single entry, see +[Get a cron entry](get-cron-entry.md). To update one, see +[Update a cron entry](update-cron-entry.md). + +## Code samples + +### Request example + +{title="GET /v1/instances/{instance_id}/cron"} +```bash +curl https://api.amezmo.com/v1/instances/{instance_id}/cron \ + -H "Authorization: Bearer $AMEZMO_API_KEY" +``` + +### Response + +{title="200 OK"} +```javascript +[ + { + "id": 412, + "name": "Laravel-task-scheduler", + "command": "#!/bin/bash\nphp artisan schedule:run", + "expression": "* * * * *", + "expression_alias": "@minutely", + "status": "Created", + "environment_name": "production", + "log_file_path": "/home/deployer/cron/logs/cron.Laravel-task-scheduler.log", + "created_at": "2026-02-11T18:04:22.000000Z", + "updated_at": "2026-09-10T14:31:07.000000Z" + } +] +``` diff --git a/api/cron/update-cron-entry.md b/api/cron/update-cron-entry.md new file mode 100644 index 0000000..5d973e4 --- /dev/null +++ b/api/cron/update-cron-entry.md @@ -0,0 +1,87 @@ +# Update a cron entry + +{.lead} +Update the **script** a cron entry runs, its **schedule**, or both, without +recreating the entry. + +`PATCH` /v1/instances/{instance_id}/cron/{cron_id} + +## Parameters + +Parameter | Type | In | Required | Description +----------- | ------ | ---- | ----------- | ------------------------------------------------------- +instance_id | string | uri | Yes | The instance ID +cron_id | string | uri | Yes | The cron entry ID. See [List cron entries](list-cron-entries.md) +command | string | body | Conditional | The bash script to run. Required when you omit `expression` +expression | string | body | Conditional | The schedule. Required when you omit `command` + +Send `command`, `expression`, or both. A field you leave out keeps its current +value, and a request that carries neither returns a validation error. + +Use `--data-urlencode` rather than `--data` for both fields. A script contains +newlines and an expression contains spaces and `*`, and only the encoding form +sends those intact. + +Read the current values first with +[Get a cron entry](get-cron-entry.md), so a partial update starts from what is +actually on the instance. + +`expression` accepts a five-field POSIX cron expression or one of the +[schedule aliases](../../cron/schedules.md), such as `@minutely` or +`@every_5_minutes`. Not every expression the API accepts runs on a Linux +instance: see +[custom cron expressions](../../cron/custom-expressions.md) for the extensions +to avoid. + +The response reports the resolved five-field `expression` along with the +matching `expression_alias`, so an alias you send back comes out as both. + +> [!NOTE] +> A cron entry's `name` is fixed. Amezmo derives the entry's log file name from +> it, so renaming would leave the existing log behind under the old name. To +> change a name, delete the entry from the dashboard and +> [create a new one](create-cron-entry.md). +> [Editing a cron entry](../../cron/editing-entries.md) covers the same +> limitations for the dashboard form. + +Amezmo applies the change on your instance asynchronously: it rewrites the +script and the crontab entry in place, so the entry keeps its ID and its log +file. A run already in flight finishes under the old script. + +Updating an entry that is being deleted returns `409 Conflict` with the code +`cron_entry_deleting`. An instance whose type does not include cron returns +`422` with `feature_not_supported`. See [Errors](../errors.md) for the shape of +both. + +## Code samples + +### Request example + +{title="PATCH /v1/instances/{instance_id}/cron/{cron_id}"} +```bash +curl https://api.amezmo.com/v1/instances/{instance_id}/cron/412 \ + -X PATCH \ + -H "Authorization: Bearer $AMEZMO_API_KEY" \ + --data-urlencode command="#!/bin/bash +php artisan schedule:run --no-interaction +" \ + --data-urlencode expression='@every_5_minutes' +``` + +### Response + +{title="200 OK"} +```javascript +{ + "id": 412, + "name": "Laravel-task-scheduler", + "command": "#!/bin/bash\nphp artisan schedule:run --no-interaction", + "expression": "*/5 * * * *", + "expression_alias": "@every_5_minutes", + "status": "Created", + "environment_name": "production", + "log_file_path": "/home/deployer/cron/logs/cron.Laravel-task-scheduler.log", + "created_at": "2026-02-11T18:04:22.000000Z", + "updated_at": "2026-09-10T14:31:07.000000Z" +} +``` diff --git a/api/deployments/cancel-deployment.md b/api/deployments/cancel-deployment.md index bc28dc4..d787eef 100644 --- a/api/deployments/cancel-deployment.md +++ b/api/deployments/cancel-deployment.md @@ -9,8 +9,8 @@ POST /v1/instances/{instance_id}/deployments/{deployment_id}/cancel Parameter | Type | In | Required | Description ------------- | ------ | --- | -------- | ---------------------------------- -instance_id | string | uri | No | The instance id of the environment -deployment_id | string | uri | No | The deployment ID +instance_id | string | uri | Yes | The instance id of the environment +deployment_id | string | uri | Yes | The deployment ID ## Response diff --git a/api/deployments/get.md b/api/deployments/get.md index 0cdab1e..d93d7ab 100644 --- a/api/deployments/get.md +++ b/api/deployments/get.md @@ -9,8 +9,8 @@ GET /v1/instances/{instance_id}/deployments/{deployment_id} Parameter | Type | In | Required | Description ------------- | ------ | --- | -------- | ---------------------------------- -instance_id | string | uri | No | The instance id of the environment -deployment_id | string | uri | No | The deployment ID +instance_id | string | uri | Yes | The instance id of the environment +deployment_id | string | uri | Yes | The deployment ID ## Response diff --git a/api/deployments/post.md b/api/deployments/post.md index 2e5eded..32b30af 100644 --- a/api/deployments/post.md +++ b/api/deployments/post.md @@ -31,7 +31,7 @@ Parameter | Type | In | Required | Description api_key | string | header | Yes | Your [API key](../authentication/index.md). environment | string | body | Yes | The [environment](../environments/index.md) name for this deployment. This can be `production` or `staging`. instance_id | string | uri | Yes | The ID of the instance that this deployment will be executed on. -archive | body | uri | Yes | The archive file that contains the source code of your application. This can be a zip or a tar archive. The maximum size of an archive file is 512MB. +archive | file | body | Yes | The archive file that contains the source code of your application. This can be a zip or a tar archive. The maximum size of an archive file is 512MB. repo_owner | string | body | Conditional | The repository owner. Required if `repo_name` is provided. repo_name | string | body | Conditional | The repository name. Required if `repo_owner` is provided. branch | string | body | No | The name of the branch diff --git a/api/endpoints.md b/api/endpoints.md index de86e7b..7eca8ff 100644 --- a/api/endpoints.md +++ b/api/endpoints.md @@ -27,6 +27,26 @@ so you set it once instead of pasting it into every command: export AMEZMO_API_KEY="your-api-key" ``` +## Errors + +A failing request returns a JSON body with the same fields every time: a `type` +to branch on, a specific `code`, a human `message`, and a `request_id` to quote +when you report a problem. See [Errors](errors.md). + +{title="404 Not Found"} +```javascript +{ + "http_status": 404, + "type": "invalid_request_error", + "code": "unknown_resource_error", + "message": "The requested resource does not exist or you do not have permission to access it.", + "errors": {}, + "doc_url": "https://www.amezmo.com/docs/api", + "request_id": "req_01HZX9C4Q2", + "error": null +} +``` + ## Example Request This request lists the regions where you can launch an instance. The double diff --git a/api/environments/get-environment.md b/api/environments/get-environment.md index c667810..de0aa16 100644 --- a/api/environments/get-environment.md +++ b/api/environments/get-environment.md @@ -6,8 +6,8 @@ ## Parameters Parameter | Type | In | Required | Description ----------- | ------ | --- | -------- | ---------------------------------- -instance_id | string | uri | No | The instance id of the environment -name | string | uri | No | The name of the environment +instance_id | string | uri | Yes | The instance id of the environment +name | string | uri | Yes | The name of the environment ## Response diff --git a/api/environments/list-environments.md b/api/environments/list-environments.md index 8513c53..f860293 100644 --- a/api/environments/list-environments.md +++ b/api/environments/list-environments.md @@ -6,7 +6,7 @@ ## Parameters Parameter | Type | In | Required | Description ----------- | ------ | --- | -------- | --------------- -instance_id | string | uri | No | The instance id +instance_id | string | uri | Yes | The instance id ## Response diff --git a/api/environments/update-environment.md b/api/environments/update-environment.md index 03f1639..54e5ee1 100644 --- a/api/environments/update-environment.md +++ b/api/environments/update-environment.md @@ -2,7 +2,7 @@ {.lead} Update the settings of an **environment** on an instance, such as auto-deploy -patterns, SSH access and the New Relic license key. +patterns, SSH access, and the New Relic license key. `PATCH` /v1/instances/{instance_id}/environments/{name} @@ -20,7 +20,7 @@ trusted_ssh_ips | array | body | No | An array of IPv4 addre When you update `newrelic_license_key`, the change takes effect on the next [deployment](../deployments/index.md). A `null` value disables the New Relic Application Performance Monitoring -(<abbr title="Application Performance Monitoring">APM</abbr>) integration. Amezmo encrypts the key at rest, decrypts it at instance creation +(<abbr title="Application Performance Monitoring">APM</abbr>) integration. Amezmo encrypts the key at rest, decrypts it at instance creation, and stores it in the `newrelic.ini` PHP configuration file. You can read the stored value with `php --ri newrelic | grep newrelic.license`. diff --git a/api/errors.md b/api/errors.md new file mode 100644 index 0000000..5d1b8f2 --- /dev/null +++ b/api/errors.md @@ -0,0 +1,96 @@ +# Errors + +{.lead} +Every Amezmo API error returns the same JSON body, whatever went wrong. Branch on +`type` to decide how to handle it and on `code` when you need the exact reason. + +{title="422 Unprocessable Entity"} +```javascript +{ + "http_status": 422, + "type": "invalid_request_error", + "code": "validation_error", + "message": "The request you provided failed validation.", + "errors": { + "expression": "This is an invalid cron expression." + }, + "doc_url": "https://www.amezmo.com/docs/api/cron/update-cron-entry", + "request_id": "req_01HZX9C4Q2", + "error": null +} +``` + +Field | Description +------------ | ----------------------------------------------------------------- +`http_status`| The HTTP status, repeated in the body so it survives logging +`type` | The coarse class of error. A short, closed set, listed below +`code` | The specific reason. Open-ended, so treat an unfamiliar one as its `type` +`message` | A human-readable explanation. Not a stable interface: don't match on it +`errors` | Per-parameter messages, keyed by parameter name. `{}` when not a validation failure +`doc_url` | The reference page for whatever failed +`request_id` | Identifies this request. Quote it when you contact support +`error` | Debugging detail, always `null` outside development + +## Types + +`invalid_request_error` +: Something about the request is wrong: a bad parameter, a resource that doesn't +exist, a state that forbids the action, or an unmet account requirement. Retrying +the same request unchanged fails the same way. + +`authentication_error` +: Your API key is missing, malformed, disabled, or not allowed to do this. See +[Authentication](authentication/index.md). + +`rate_limit_error` +: You sent too many requests. Back off and retry. + +`api_error` +: Something failed on Amezmo's side. These are safe to retry. + +## Codes + +The codes you're most likely to handle. Individual endpoints add their own for +situations only they can hit, documented on that endpoint's page. New codes get +added as the API grows, so treat an unrecognized one as its `type` rather than +failing. + +Code | Status | Meaning +---------------------------- | ------ | ------------------------------------------- +`validation_error` | 422 | One or more parameters failed validation. See `errors` +`unknown_resource_error` | 404 | No such resource, or your key can't see it +`method_not_allowed` | 405 | Wrong HTTP method for this path +`conflict` | 409 | The resource's current state forbids this +`unauthenticated` | 401 | No usable API key on the request +`invalid_credentials` | 401 | The API key was rejected +`forbidden` | 403 | The key is valid but not allowed to do this +`email_verification_required`| 403 | Verify your email address first +`payment_method_required` | 402 | Add a payment method first +`payment_method_flagged` | 402 | Your payment method could not be verified +`account_past_due` | 402 | Settle an outstanding balance first +`feature_not_supported` | 422 | Your instance type doesn't include this feature +`invalid_instance_state` | 409 | The environment is paused, launching, or failed +`instance_in_progress` | 422 | Another instance is still being created +`instance_limit_reached` | 422 | You're at your account's instance limit +`rate_limit_exceeded` | 429 | Too many requests +`internal_error` | 500 | Something failed on Amezmo's side + +## Validation errors + +A `validation_error` fills in `errors` with one message per rejected parameter, +keyed by the parameter's name. Everything else leaves `errors` as `{}`. + +{title="Reading a validation failure"} +```bash +$ curl -s https://api.amezmo.com/v1/instances/{instance_id}/cron/412 \ + -X PATCH \ + -H "Authorization: Bearer $AMEZMO_API_KEY" \ + --data-urlencode expression='every other tuesday' \ + | python3 -c 'import json,sys; print(json.load(sys.stdin)["errors"])' +``` + +## Reporting a problem + +`request_id` identifies one request on Amezmo's side. Include it when you write +to [support](../support/index.md), along with the endpoint and roughly when you +called it. diff --git a/api/index.md b/api/index.md index 2b0c61d..3b2b1e8 100644 --- a/api/index.md +++ b/api/index.md @@ -17,8 +17,18 @@ Every request carries a Bearer token. [API authentication](authentication/index. covers where to find your key and how the examples store it in an `AMEZMO_API_KEY` environment variable. +### Errors + +Every failure returns the same JSON body. [Errors](errors.md) documents that +envelope, the error types worth branching on, and the codes behind them. + ## Core Resources +### Accounts + +The user behind your API key. [Account endpoints](accounts/index.md) get the +current user. + ### Workers Start, stop, and restart [worker processes](workers/index.md) through the API. @@ -32,15 +42,20 @@ types, and terminate an instance. ### Environments An environment ties an instance to a Git repository and its deployment rules. -[Environment endpoints](environments/index.md) read and update those, including -automatic deployment settings. +[Environment endpoints](environments/index.md) list, get, and update those, +including automatic deployment settings. ### Deployments Deploy without Git by supplying a `.zip` archive of your source. -[Deployment endpoints](deployments/index.md) create, read, and cancel +[Deployment endpoints](deployments/index.md) create, get, and cancel deployments in both staging and production. +### Cron + +List, get, create, and update the [cron entries](cron/index.md) on an +instance. + ### Regions [Region endpoints](regions/index.md) list the regions an instance can run in diff --git a/api/instances/get-instance.md b/api/instances/get-instance.md index f4de670..6c8ebe9 100644 --- a/api/instances/get-instance.md +++ b/api/instances/get-instance.md @@ -1,7 +1,7 @@ # Get an instance {.lead} -Retrieve a single **instance** by its id, including its runtime configuration +Get a single **instance** by its id, including its runtime configuration and every environment attached to it. `GET` /v1/instances/{instance_id} diff --git a/api/instances/terminate-instance.md b/api/instances/terminate-instance.md index c5eddad..64e5eb0 100644 --- a/api/instances/terminate-instance.md +++ b/api/instances/terminate-instance.md @@ -8,7 +8,7 @@ API requests for this instance will respond with 404 not found. ## Parameters Parameter | Type | In | Required | Description ----------- | ------ | --- | -------- | --------------- -instance_id | string | uri | No | The instance id +instance_id | string | uri | Yes | The instance id ## Code samples diff --git a/api/regions/get-region.md b/api/regions/get-region.md index 62dcbdd..184f4d7 100644 --- a/api/regions/get-region.md +++ b/api/regions/get-region.md @@ -1,7 +1,7 @@ # Get a region {.lead} -Retrieve a single **region** by its id, including its name and ISO country code. +Get a single **region** by its id, including its name and ISO country code. Use it to confirm a region before you launch an instance there. `GET` /v1/regions/{region_id} diff --git a/api/regions/list-regions.md b/api/regions/list-regions.md index d49bf55..fc6b629 100644 --- a/api/regions/list-regions.md +++ b/api/regions/list-regions.md @@ -19,7 +19,7 @@ curl https://api.amezmo.com/v1/regions \ ### Response -The response is an array of regions. Retrieve one region with +The response is an array of regions. To get a single region, see [Get a region](get-region.md). {title="200 OK"} diff --git a/api/workers/get.md b/api/workers/get.md index b699be1..50ea596 100644 --- a/api/workers/get.md +++ b/api/workers/get.md @@ -6,8 +6,8 @@ ## Parameters Parameter | Type | In | Required | Description ----------- | ------ | --- | -------- | --------------- -instance_id | string | uri | No | The instance ID -worker_id | string | uri | No | The worker ID +instance_id | string | uri | Yes | The instance ID +worker_id | string | uri | Yes | The worker ID ## Response diff --git a/api/workers/restart.md b/api/workers/restart.md index ae917a3..fb9b803 100644 --- a/api/workers/restart.md +++ b/api/workers/restart.md @@ -7,8 +7,8 @@ ## Parameters Parameter | Type | In | Required | Description ----------- | ------ | --- | -------- | --------------- -instance_id | string | uri | No | The instance ID -worker_id | string | uri | No | The worker ID +instance_id | string | uri | Yes | The instance ID +worker_id | string | uri | Yes | The worker ID diff --git a/api/workers/start.md b/api/workers/start.md index 79c1055..6f6698e 100644 --- a/api/workers/start.md +++ b/api/workers/start.md @@ -6,8 +6,8 @@ ## Parameters Parameter | Type | In | Required | Description ----------- | ------ | --- | -------- | --------------- -instance_id | string | uri | No | The instance ID -worker_id | string | uri | No | The worker ID +instance_id | string | uri | Yes | The instance ID +worker_id | string | uri | Yes | The worker ID ## Response diff --git a/api/workers/stop.md b/api/workers/stop.md index cf2d815..3b6cca4 100644 --- a/api/workers/stop.md +++ b/api/workers/stop.md @@ -6,8 +6,8 @@ ## Parameters Parameter | Type | In | Required | Description ----------- | ------ | --- | -------- | --------------- -instance_id | string | uri | No | The instance ID -worker_id | string | uri | No | The worker ID +instance_id | string | uri | Yes | The instance ID +worker_id | string | uri | Yes | The worker ID diff --git a/billing/credits.md b/billing/credits.md index b974905..abbf4cd 100644 --- a/billing/credits.md +++ b/billing/credits.md @@ -22,7 +22,7 @@ your next monthly invoice. See [Hourly billing](hourly.md) for when invoices go out. To review the promotions you've redeemed, open Billing > Credits. Each one -lists its date, code, amount and expiration. +lists its date, code, amount, and expiration. ## Why a Code Won't Apply diff --git a/billing/payment-methods.md b/billing/payment-methods.md index 4424db3..1cfbb5c 100644 --- a/billing/payment-methods.md +++ b/billing/payment-methods.md @@ -1,11 +1,11 @@ # Payment Methods Manage the cards on your account from Billing > Payment methods. You can add a -card, choose your default and remove cards you no longer use. +card, choose your default, and remove cards you no longer use. ## Adding a Card -Open Billing > Payment methods, enter your card details and save. Your first +Open Billing > Payment methods, enter your card details, and save. Your first card becomes your default automatically. Amezmo charges the default card for your monthly [hourly usage](hourly.md). @@ -21,7 +21,7 @@ add the new card and set it as the default first, then remove the old one. ## Fixing a Past-Due or Failed Payment -A charge can fail for a few reasons, like an expired card, insufficient funds +A charge can fail for a few reasons, like an expired card, insufficient funds, or a charge your bank blocked. When that happens, your account becomes past due and the dashboard shows a banner asking you to update your billing details. @@ -33,7 +33,7 @@ there's no separate pay button. Once a retry succeeds, the past-due state clears on its own. If the same card keeps failing, add a different card instead of retrying it. -While your account is past due you can't launch, start or resize +While your account is past due you can't launch, start, or resize [instances](../instances/index.md), and Amezmo blocks API access. > [!WARNING] diff --git a/cron/custom-expressions.md b/cron/custom-expressions.md new file mode 100644 index 0000000..a859995 --- /dev/null +++ b/cron/custom-expressions.md @@ -0,0 +1,143 @@ +# Custom cron expressions + +{.lead} +When none of the [schedule aliases](schedules.md) fits, write a five-field +cron expression instead. This page covers the syntax Amezmo accepts, the parts +of it that look valid but never run, and a set of expressions you can copy. + +In the dashboard, open a cron entry's schedule dropdown, choose **Custom**, and +type the expression into the dialog. Over the API, send the same string as the +`expression` field of +[Create a cron entry](../api/cron/create-cron-entry.md) or +[Update a cron entry](../api/cron/update-cron-entry.md). + +## The five fields + +An expression is five fields separated by spaces. Each field says which values of +that unit the entry runs on, and the entry runs when every field matches. + +{title="Field order"} +```bash +# minute hour day-of-month month day-of-week + 30 2 * * * +``` + +Field | Range | Notes +------------ | ------- | ----------------------------------------------- +minute | `0-59` | +hour | `0-23` | Midnight is `0`, not `24` +day of month | `1-31` | +month | `1-12` | Or `JAN` through `DEC` +day of week | `0-7` | `0` and `7` are both Sunday. Or `SUN` through `SAT` + +Names are not case sensitive, so `mon` and `MON` both work. You can't use a name +in a step, so write `1-5` rather than `MON-FRI/1`. + +## Operators + +`*` +: Every value of the field. `* * * * *` is every minute of every day. + +`,` +: A list. `0 8,12,18 * * *` runs at 08:00, 12:00, and 18:00. + +`-` +: A range. `0 9-17 * * *` runs hourly from 09:00 through 17:00, inclusive. + +`/` +: A step through a range. `*/15 * * * *` runs at minutes 0, 15, 30, and 45. +`0 9-17/2 * * *` runs at 09:00, 11:00, 13:00, 15:00, and 17:00. + +A step counts from the start of its range, not from the current time. `*/40` in +the minute field fires at minute 0 and minute 40, then waits 20 minutes, because +the range restarts every hour. Steps that don't divide evenly into their range +are uneven like this, which is why `*/15` is a safer habit than `*/40`. + +## Examples + +Expression | Runs +--------------- | ------------------------------------------------ +`*/10 * * * *` | Every ten minutes +`0 * * * *` | At the top of every hour +`30 2 * * *` | Every day at 02:30 +`0 9-17 * * 1-5`| Hourly from 09:00 to 17:00, Monday through Friday +`0 3 * * 0` | Sundays at 03:00 +`0 0 1 * *` | Midnight on the first of the month +`0 0 1 1 *` | Midnight on January 1 +`15 4 1,15 * *` | 04:15 on the 1st and the 15th + +## Limitations + +### Amezmo accepts syntax your instance won't run + +The dashboard validates your expression with a PHP parser that understands +Quartz-style extensions. Standard Linux cron, which is what actually runs the +entry on your instance, does not. So these save without complaint and then never +fire: + +Not supported | What it means elsewhere +------------- | ------------------------------------- +`L` | Last day of the month or the week +`W` | Nearest weekday to a date, as in `15W` +`#` | Nth weekday of the month, as in `5#3` +`?` | No specific value + +Stick to `*`, `,`, `-`, `/`, numbers, and month or weekday names. If you need +"last day of the month", schedule the entry daily and exit early from your +script instead: + +{title="Run only on the last day of the month"} +```bash +#!/bin/bash +# Tomorrow is the 1st, so today is the last day of this month. +if [ "$(date -d tomorrow +%d)" != "01" ]; then + exit 0 +fi + +php artisan billing:close-month +``` + +### Day of month and day of week are an OR, not an AND + +When both the day-of-month and the day-of-week fields are restricted, cron runs +the entry when **either** matches, not when both do. `0 0 13 * 5` runs on the +13th of every month *and* on every Friday, not only on Friday the 13th. Leave one +of the two as `*` unless you want that behavior. + +### `@reboot` and seconds are not supported + +Amezmo rejects `@reboot`: a cron entry is a schedule, not a startup hook. Use a +[worker](../workers/index.md) for a process that should run continuously and come +back after a restart. + +There is no seconds field. One minute is the shortest interval an expression can +express. If you need to act more often than that, loop inside your script or run +a worker. + +### Schedules follow the instance clock + +Cron matches your expression against your instance's system clock, and you can't +set a timezone per entry. Check what your instance thinks the time is over +[SSH](../instances/ssh.md): + +{title="Check the instance clock"} +```bash +$ date +``` + +If your task's timing has to follow a specific timezone, set `TZ` inside the +script for the commands it runs, and pick the expression to match the instance +clock. + +### Editing the schedule doesn't reschedule a running task + +Saving a new expression rewrites the entry on your instance in the background. A +run already underway finishes on the old schedule's terms. See +[Editing a cron entry](editing-entries.md). + +## Checking your work + +The entry page streams the task's log, so the fastest way to confirm an +expression is to save it, wait for the first window, and watch the log. Give a +new script a frequent schedule such as `@minutely` while you're testing it, then +switch to the real one. diff --git a/cron/editing-entries.md b/cron/editing-entries.md new file mode 100644 index 0000000..75a7fe2 --- /dev/null +++ b/cron/editing-entries.md @@ -0,0 +1,64 @@ +# Editing a cron entry + +{.lead} +Change the script a [cron entry](index.md) runs, or how often it runs, without +recreating the entry. + +## Editing from the dashboard + +Open your instance, go to **Cron**, and open the entry you want to change. Click +**Edit** in the entry header, or pick **Edit** from the entry's menu in the cron +list. + +The edit form is the same one you used to create the entry. Change the schedule, +the script, or both, then click **Save**. + +## What you can change + +Schedule +: Pick an alias such as `@hourly` from the dropdown, or choose **Custom** and +type an expression. See [custom cron expressions](custom-expressions.md) for the +syntax. An entry created with a custom expression comes back with that +expression already selected. + +Script +: Edit the bash script in place, or drag a `.sh` file onto the editor to replace +it. Scripts still run from your +[current release](../deployments/directories.md) directory, so a script that +worked before an edit works after one. + +## What you can't change + +**The name is fixed.** Amezmo derives the entry's log file name from the name you +gave it, so a rename would leave the existing log stranded under the old name and +start a fresh, empty one. The name field is read-only on the edit form. If you +need a different name, delete the entry and create a new one, and keep in mind +that deleting removes the old log file with it. + +**Editing doesn't move an entry between environments.** An entry belongs to the +environment it was created in. To run the same task in staging and production, +create an entry in each. + +**An entry being deleted can't be edited.** Once you confirm a delete, the entry +is on its way out and the **Edit** action disappears from it. + +## What happens when you save + +Amezmo applies the change on your instance in the background: it rewrites your +script and the crontab entry in place. The entry keeps its ID and keeps writing +to the same log file, so the history you already have stays where it is. + +The change is not instantaneous, and it is not transactional with a run in +flight. A run that has already started finishes under the old script. The next +run after the update lands uses the new one. + +> [!NOTE] +> Editing the schedule doesn't run the task. If you want to confirm a new script +> works, give it a frequent schedule such as `@minutely`, watch the log on the +> entry page, then set the schedule you actually want. + +## Editing over the API + +[Update a cron entry](../api/cron/update-cron-entry.md) does the same thing over +the REST API, with the same limitations. It takes `command`, `expression`, or +both, and leaves out anything you omit. diff --git a/cron/index.md b/cron/index.md index 7201440..2be15cd 100644 --- a/cron/index.md +++ b/cron/index.md @@ -7,33 +7,40 @@ that is required to execute your task, and the logging of the task output so you see the results in your Amezmo dashboard. Crons are executed from your applications [current release](../deployments/directories.md) directory. -## Schedules -A cron schedule represents when the task will be executed. You may use an alias for the following cron expressions. +## In This Section -`@minutely` -: Execute the task once every minute. +### Cron Schedules -`@hourly` -: Execute the task once every hour. +An alias such as `@daily` or a five-field expression decides when an entry +runs. [Cron schedules](schedules.md) lists every alias with the expression it +resolves to, in the dashboard and over the API. -`@monthly` -: Execute the task once every month. +### Custom Cron Expressions -`@weekly` -: Execute the task once every week. +When no alias fits, write a five-field expression instead. +[Custom cron expressions](custom-expressions.md) covers the field order and the +operators, a table of ready-made schedules, and the syntax that saves but never +runs on your instance. -`@yearly` -: Execute the task once every year. +### Editing a Cron Entry -For advanced use cases, -Amezmo supports [POSIX cron syntax](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07). +Change an entry's script or its schedule after you create it, from the dashboard +or the API. [Editing a cron entry](editing-entries.md) covers what you can change, +why the name is fixed, and when the change reaches your instance. ## Scripts A cron *script* is a bash script where you define your task logic. In this script, you may call into other -services such as PHP, Node.js and anything you'd normally do from the command line. Scripts run on your instance and in the current working +services such as PHP, Node.js, and anything you'd normally do from the command line. Scripts run on your instance and in the current working directory as your most recent [release](../deployments/releases.md). ## Best practices It's best to use a Cron job as a means to invoke another script. Don't put any logic into your Cron job. Keep your business logic in your [git](../git/index.md) repository so that any updates won't require an update to your Cron job as well. +That pays off when a task changes: you deploy the new logic instead of +[editing the cron entry](editing-entries.md). + +## API + +The Amezmo API lists, gets, creates, and updates your cron entries. See +[Cron endpoints](../api/cron/index.md). diff --git a/cron/schedules.md b/cron/schedules.md new file mode 100644 index 0000000..71a4f52 --- /dev/null +++ b/cron/schedules.md @@ -0,0 +1,38 @@ +# Cron Schedules + +{.lead} +A cron schedule says when a [cron entry](index.md) runs: an alias such as +`@daily`, or a five-field POSIX cron expression. + +## Aliases + +Use an alias in place of its cron expression: + +Alias | Expression | Runs +------------------ | ------------- | --------------------------- +`@minutely` | `* * * * *` | Every minute +`@every_5_minutes` | `*/5 * * * *` | Every five minutes +`@hourly` | `0 * * * *` | At the top of every hour +`@daily` | `0 0 * * *` | Every day at midnight +`@weekly` | `0 0 * * 0` | Every Sunday at midnight +`@monthly` | `0 0 1 * *` | On the first of every month +`@yearly` | `0 0 1 1 *` | On January 1 +`@annually` | `0 0 1 1 *` | On January 1 + +`@yearly` and `@annually` resolve to the same expression, so pick either. +The dashboard's schedule dropdown offers the same aliases. + +## Custom Expressions + +When no alias fits, +Amezmo supports [POSIX cron syntax](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07). +See [custom cron expressions](custom-expressions.md) for the field order, the +operators, and the extensions to avoid. + +## Schedules over the API + +[Create a cron entry](../api/cron/create-cron-entry.md) and +[update a cron entry](../api/cron/update-cron-entry.md) take a schedule in +their `expression` parameter, as an alias or a five-field expression. The +response reports the resolved five-field `expression` along with the matching +`expression_alias`, so an alias you send comes out as both. diff --git a/databases/backup-restore.md b/databases/backup-restore.md index 9d6a274..2af8bf2 100644 --- a/databases/backup-restore.md +++ b/databases/backup-restore.md @@ -2,7 +2,7 @@ Amezmo takes logical backups of your MySQL database, the kind you'd get from ``mysqldump``: a file of SQL statements you can restore anywhere. Use a backup -to copy production to your machine, to archive or to move data between +to copy production to your machine, to archive, or to move data between environments. ## On-Demand Backups diff --git a/databases/index.md b/databases/index.md index f99181c..6bc982b 100644 --- a/databases/index.md +++ b/databases/index.md @@ -1,7 +1,7 @@ # Databases Amezmo runs a dedicated MySQL server (5.7 or 8.0) for instances launched with -database support. From the dashboard you can back up, restore and check the +database support. From the dashboard you can back up, restore, and check the status of your database, and any instance shared with the database instance can reach it. Your MySQL server is never reachable from the public internet. @@ -15,7 +15,7 @@ Back up, restore, and check its status from the dashboard. ### Redis [Redis](redis.md) is available if you chose it at instance creation, reachable -from your shared instances and never exposed to the internet. Data is not +from your shared instances, and never exposed to the internet. Data is not persisted, so treat it as an in-memory cache. ### Remote Access diff --git a/databases/ssh.md b/databases/ssh.md index 1170c9e..709fe4b 100644 --- a/databases/ssh.md +++ b/databases/ssh.md @@ -2,7 +2,7 @@ If you launched an instance with MySQL support, the database is reachable from that instance and from any instance shared with it. From the Amezmo dashboard -you can back up, restore and check its status. +you can back up, restore, and check its status. First make sure [SSH is enabled](../instances/enable-or-disable-ssh.md) on your instance, then go to Overview, then Server Details, then find your SSH port. An diff --git a/domains/rules.md b/domains/rules.md index 6821298..e78ed2b 100644 --- a/domains/rules.md +++ b/domains/rules.md @@ -12,7 +12,7 @@ process. See 2. Open the create-rule form and enter the path. 3. Choose the Websocket action. -The path accepts letters, digits, ``_``, ``-`` and ``*``, with no slashes. +The path accepts letters, digits, ``_``, ``-``, and ``*``, with no slashes. Amezmo adds the leading slash for you. To remove a rule, delete it from the domain's rules table. diff --git a/domains/wildcard-ssl-certificates.md b/domains/wildcard-ssl-certificates.md index bd58d8c..30f25b0 100644 --- a/domains/wildcard-ssl-certificates.md +++ b/domains/wildcard-ssl-certificates.md @@ -3,7 +3,7 @@ {.lead} A **wildcard SSL certificate** secures a domain and all of its direct subdomains with one certificate, so `*.example.com` covers `app.example.com`, -`api.example.com` and every other subdomain at that level. Amezmo issues +`api.example.com`, and every other subdomain at that level. Amezmo issues wildcard certificates from Let's Encrypt, the same authority it uses for [standard certificates](ssl-certificates.md). diff --git a/environments/cloning.md b/environments/cloning.md index a699089..610c98c 100644 --- a/environments/cloning.md +++ b/environments/cloning.md @@ -9,10 +9,10 @@ On Advanced instances you can clone a full environment from the instance Actions menu. Pick the environment to copy from, give the new environment a unique name, and choose its branch. Amezmo copies: -- The Git connection, deploy key and chosen branch. +- The Git connection, deploy key, and chosen branch. - The environment variables. - The database, including schema and data. -- Deployment settings, health checks, alerts and trusted IP addresses. +- Deployment settings, health checks, alerts, and trusted IP addresses. The new environment then runs a deployment. Cloning a full environment is only available on Advanced instances. diff --git a/git/providers.md b/git/providers.md index 7ef5bd3..ecb0f74 100644 --- a/git/providers.md +++ b/git/providers.md @@ -1,11 +1,11 @@ # Git Providers -Amezmo is a Git-native deployment platform. You can deploy from GitHub, GitLab +Amezmo is a Git-native deployment platform. You can deploy from GitHub, GitLab, and Bitbucket. You connect a provider from the Git tab. Amezmo asks you to authorize with the -provider, stores an access token for that account and uses it to list your -repositories, add a deploy key and create a webhook for automatic deployments. +provider, stores an access token for that account, and uses it to list your +repositories, add a deploy key, and create a webhook for automatic deployments. ## Permissions Amezmo Needs diff --git a/instances/index.md b/instances/index.md index 4bec3b1..da9ba7d 100644 --- a/instances/index.md +++ b/instances/index.md @@ -8,7 +8,7 @@ action in the dashboard runs in the context of one environment. Everything you need to run a production PHP app is set up by default, with little or no custom configuration. Amezmo includes framework-specific -configuration for CraftCMS, Laravel and Yii. +configuration for CraftCMS, Laravel, and Yii. For pricing and features, see the [pricing](/pricing) page. diff --git a/metrics/index.md b/metrics/index.md index 9e2513f..79e65aa 100644 --- a/metrics/index.md +++ b/metrics/index.md @@ -1,6 +1,6 @@ # Metrics -The Metrics tab shows how your environment is performing: CPU, memory and HTTP +The Metrics tab shows how your environment is performing: CPU, memory, and HTTP request stats for the environment you're viewing. ## CPU diff --git a/nginx/compression-and-caching.md b/nginx/compression-and-caching.md index b32a77c..22f57de 100644 --- a/nginx/compression-and-caching.md +++ b/nginx/compression-and-caching.md @@ -4,7 +4,7 @@ The Nginx tab has two switches that speed up how your site serves assets. ## Gzip Compression -Turn on Gzip to compress text assets like JavaScript, CSS and HTML before Nginx +Turn on Gzip to compress text assets like JavaScript, CSS, and HTML before Nginx sends them, which cuts download size. Amezmo supports Gzip. There's no Brotli option. diff --git a/nginx/location-blocks.md b/nginx/location-blocks.md index 0370462..5438232 100644 --- a/nginx/location-blocks.md +++ b/nginx/location-blocks.md @@ -2,11 +2,11 @@ A location block is an Nginx ``location`` directive scoped to your app. Amezmo generates a default set based on your app type (Laravel, Craft, WordPress, -Drupal or plain PHP), and you can edit them or add your own from the Nginx tab. +Drupal, or plain PHP), and you can edit them or add your own from the Nginx tab. ## Editing a Block -You can edit the body of a block, add a block or delete one. The ``location`` +You can edit the body of a block, add a block, or delete one. The ``location`` line itself is fixed, so to change the match you delete the block and add a new one. When you add a block, put the body on its own line after the opening ``{``. @@ -39,6 +39,6 @@ To route a whole domain to a worker process instead, see ## Coming from .htaccess Amezmo runs Nginx, not Apache, so it ignores ``.htaccess`` files. Move your -rewrite, redirect, deny and expires rules into location blocks or your custom +rewrite, redirect, deny, and expires rules into location blocks or your custom [Nginx configuration](config.md). For password protection, use [HTTP authentication](http-authentication.md).