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
11 changes: 7 additions & 4 deletions .drun/spec.drun
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

version: 2.0

project "Drun" version "2.0":
project "Drun" version "2.27.0":

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat :D

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add a validation gate for this in the future, since we're now tracking the version.

requires tools:
go >= "1.26.5"
golangci-lint provision
Expand Down Expand Up @@ -98,14 +98,17 @@ task "ci" mode "ci" means "Runs the whole CI pipeline":
success "CI executed successfully end-to-end"

task "prepare-release" means "Runs checks related to going to a next release":
requires $release_version as string matching semver_optional_v
step "Preflight. Proposed version: v{$release_version}"
requires $version as string matching semver_optional_v
step "Preflight. Proposed version: v{$version}"
# Byte for byte comparison
if file "./drun.tmLanguage.json" not matches file "./docs/docs/assets/languages/drun.tmLanguage.json":
fail "There are different vendored versions of the TextMate grammar in the repo.\nPlease make sure they match each other before merging."
# Magic method for comparison (you can do this manually if you want to)
git ensure $release_version is newer than latest version from drun-main
git ensure $version is newer than latest version from drun-main
step "Preflight checks have passed. Will now run the CI."
call task ci
step "Promoting unreleased changelog entries to {$version}"
promote changelog "CHANGELOG.md" to version "{$version}"
update project version to "{$version}"
success "According to the current spec, you should be good to go."
warning "But be mindful, with great power, comes great responsibility."
37 changes: 37 additions & 0 deletions CHANGELOG.md

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FINALLY!

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Changelog

All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

### Changed

### Deprecated

### Removed

### Fixed

### Security

## [2.27.0] - 2026-08-10

### Added

- Added the `promote changelog` statement for [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) release management: `promote changelog "CHANGELOG.md" to version "X.Y.Z" [on "YYYY-MM-DD"]` moves the `## [Unreleased]` entries into a dated release section, leaves an emptied `Unreleased` skeleton behind, and rewrites `[Unreleased]: .../compare/<prev>...HEAD` comparison links when present. Re-running it for a version whose release section already exists merges new `Unreleased` entries into that section (a no-op when there is nothing new), so release preparation tasks stay idempotent. Honors dry runs and writes atomically with preserved permissions.
- Added LSP support for `promote changelog`: keyword completion and hover documentation with examples.
- Added TextMate grammar coverage for `promote changelog` in the vendored language artifacts.

### Changed

### Deprecated

### Removed

### Fixed

### Security
87 changes: 85 additions & 2 deletions docs/docs/assets/languages/drun.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@
{
"include": "#file-value-actions"
},
{
"include": "#changelog-actions"
},
{
"include": "#http-actions"
},
Expand Down Expand Up @@ -1027,6 +1030,50 @@
}
]
},
"changelog-actions": {
"patterns": [
{
"name": "meta.changelog.promote.drun",
"match": "^(\\s*)(promote)(\\s+)(changelog)(\\s+)(\"(?:[^\"\\\\]|\\\\.)*\")(\\s+)(to)(\\s+)(version)(\\s+)(\"(?:[^\"\\\\]|\\\\.)*\")(?:(\\s+)(on)(\\s+)(\"(?:[^\"\\\\]|\\\\.)*\"))?",
"captures": {
"2": {
"name": "support.type.action.drun"
},
"4": {
"name": "support.constant.domain.drun"
},
"6": {
"name": "string.quoted.double.path.drun",
"patterns": [
{
"include": "#interpolation"
}
]
},
"8": {
"name": "keyword.operator.word.drun"
},
"10": {
"name": "keyword.operator.word.drun"
},
"12": {
"name": "string.quoted.double.drun",
"patterns": [
{
"include": "#interpolation"
}
]
},
"14": {
"name": "keyword.operator.word.drun"
},
"16": {
"name": "string.quoted.double.date.drun"
}
}
}
]
},
"download-actions": {
"patterns": [
{
Expand Down Expand Up @@ -1055,6 +1102,42 @@
},
"network-actions": {
"patterns": [
{
"name": "meta.wait.duration.drun",
"match": "^(\\s*)(wait)(\\s+)(\\d+(?:\\.\\d+)?)(\\s+)(seconds?|minutes?|hours?)\\b",
"captures": {
"2": {
"name": "support.type.action.drun"
},
"4": {
"name": "constant.numeric.drun"
},
"6": {
"name": "storage.type.time-unit.drun"
}
}
},
{
"name": "meta.wait.duration.variable.drun",
"match": "^(\\s*)(wait)(\\s+)(\\{)(\\$?[A-Za-z_][A-Za-z0-9_.-]*)(\\})(\\s+)(seconds?|minutes?|hours?)\\b",
"captures": {
"2": {
"name": "support.type.action.drun"
},
"4": {
"name": "punctuation.section.interpolation.begin.drun"
},
"5": {
"name": "variable.other.drun"
},
"6": {
"name": "punctuation.section.interpolation.end.drun"
},
"8": {
"name": "storage.type.time-unit.drun"
}
}
},
{
"name": "meta.network.wait.drun",
"match": "^(\\s*)(wait)(\\s+)(for)(\\s+)(service)(\\s+)(at)(\\s+)(\"(?:[^\"\\\\]|\\\\.)*\")(\\s+)(to)(\\s+)(be)(\\s+)(ready)",
Expand Down Expand Up @@ -1614,11 +1697,11 @@
},
{
"name": "support.type.action.drun",
"match": "\\b(?:info|step|warn|warning|error|success|fail|echo|run|exec|shell|output|config|create|copy|move|delete|read|write|append|backup|check|ensure|extract|archive|build|push|pull|tag|remove|start|starting|stop|scale|deploy|rollback|wait|open|ping|test|expect|download|upload|send|receive|fetch|clone|init|switch|merge|add|commit|status|log|show|detect|search|update|restart|orchestrate|execute|apply|describe|expose)\\b"
"match": "\\b(?:info|step|warn|warning|error|success|fail|echo|run|exec|shell|output|config|create|copy|move|delete|read|write|append|backup|check|ensure|extract|archive|build|push|pull|tag|remove|start|starting|stop|scale|deploy|rollback|wait|open|ping|test|expect|download|upload|send|receive|fetch|clone|init|switch|merge|add|commit|status|log|show|detect|search|update|promote|restart|orchestrate|execute|apply|describe|expose)\\b"
},
{
"name": "support.constant.domain.drun",
"match": "\\b(?:drun|drunhub|setup|teardown|docker|image|container|compose|replicas|rollout|pods|pod|ingress|manifest|manifests|namespace|port|registry|git|github|gitlab|generic|cli|filesystem|latest|tags|series|formats|metadata|authentication|ambient|refs|date|branch|checkout|repository|remote|changes|message|files|get|post|put|delete|patch|head|options|request|response|body|headers|header|endpoint|api|data|timeout|retry|follow|redirects|verify|ssl|auth|bearer|basic|token|user|password|content|type|accept|health|healthy|service|services|provision|ready|host|connection|strategy|sequential|parallel|dependency-based|circuit|breaker|failure|threshold|recovery|interval|retries|networks|external|required|autoprovision|driver|condition|dns|tcp|domain|record|expected|ip|ips|command|working|workdir|missing|force|recreate|deps|never|always|makefile|target|args|pre|post|jobs|verbose|allocate_tty|ssh|key|fallback|delay|path|startup|shutdown|discovery|metrics|enabled|labels|unavailable|max|min|consul|etcd|server|domains|ttl|cache|memory|cpu|limit|policy|orphans|period|env_file|available|installed|tool|tools|framework|environment|node|npm|yarn|pnpm|bun|python|pip|go|golang|cargo|java|maven|gradle|ruby|gem|php|composer|rust|make|kubectl|helm|terraform|aws|gcp|azure|ci|local|production|staging|development|react|vue|angular|django|rails|express|spring|laravel|line|match|pattern|email|format|concat|split|replace|secret|trim|uppercase|lowercase|prepend|join|slice|length|keys|values|transform|subtract|multiply|divide|modulo|property|filtered|sorted|reversed|unique|first|last|basename|dirname|extension|prefix|suffix|allow|permissions|dir|file|folder|any|running|current|all|exists|locally|attached)\\b"
"match": "\\b(?:drun|drunhub|changelog|setup|teardown|docker|image|container|compose|replicas|rollout|pods|pod|ingress|manifest|manifests|namespace|port|registry|git|github|gitlab|generic|cli|filesystem|latest|tags|series|formats|metadata|authentication|ambient|refs|date|branch|checkout|repository|remote|changes|message|files|get|post|put|delete|patch|head|options|request|response|body|headers|header|endpoint|api|data|timeout|retry|follow|redirects|verify|ssl|auth|bearer|basic|token|user|password|content|type|accept|health|healthy|service|services|provision|ready|host|connection|strategy|sequential|parallel|dependency-based|circuit|breaker|failure|threshold|recovery|interval|retries|networks|external|required|autoprovision|driver|condition|dns|tcp|domain|record|expected|ip|ips|command|working|workdir|missing|force|recreate|deps|never|always|makefile|target|args|pre|post|jobs|verbose|allocate_tty|ssh|key|fallback|delay|path|startup|shutdown|discovery|metrics|enabled|labels|unavailable|max|min|consul|etcd|server|domains|ttl|cache|memory|cpu|limit|policy|orphans|period|env_file|available|installed|tool|tools|framework|environment|node|npm|yarn|pnpm|bun|python|pip|go|golang|cargo|java|maven|gradle|ruby|gem|php|composer|rust|make|kubectl|helm|terraform|aws|gcp|azure|ci|local|production|staging|development|react|vue|angular|django|rails|express|spring|laravel|line|match|pattern|email|format|concat|split|replace|secret|trim|uppercase|lowercase|prepend|join|slice|length|keys|values|transform|subtract|multiply|divide|modulo|property|filtered|sorted|reversed|unique|first|last|basename|dirname|extension|prefix|suffix|allow|permissions|dir|file|folder|any|running|current|all|exists|locally|attached)\\b"
}
]
},
Expand Down
80 changes: 80 additions & 0 deletions docs/docs/reference/language/built-in-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,54 @@ command when a file falls outside these rules.

The existing literal `replace in` action remains unchanged and independent of structured file-value operations.

#### Changelog promotion

Release tasks commonly move the `Unreleased` entries of a [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
file into a dated release section. Drun supports this directly:

```drun
promote changelog "CHANGELOG.md" to version "{$release_version}"
promote changelog "CHANGELOG.md" to version "1.5.0" on "2026-09-01"
```

The grammar is:

```text
promote changelog <file> to version <version> [on <date>]
```

The statement rewrites the file so the previous `## [Unreleased]` body becomes a new `## [<version>] - <date>`
section, inserted directly below a freshly emptied `## [Unreleased]` section that keeps the subsection skeleton it had
(`### Added`, `### Changed`, and so on). When the file carries an
`[Unreleased]: <base>/compare/<previous>...HEAD` link definition, it is rewritten to compare from the new release and
a matching `[<version>]: <base>/compare/<previous>...<version>` link is inserted below it. Files without comparison
links are promoted without link handling.

`<version>` is interpolated, tolerates a leading `v`, and must otherwise be a plain semantic version (`X.Y.Z`).
`<date>` must be a valid calendar date in `YYYY-MM-DD` form and defaults to the current local date. Promotion fails
when the file has no `## [Unreleased]` heading.

Promotion is idempotent so a release preparation can be re-run before the release is actually published: when the
`## [<version>]` section already exists, new `Unreleased` entries are merged into it (matching subsections are
appended to, new subsections are added, and the section's date and comparison links stay untouched), while an emptied
`Unreleased` section is a no-op. The statement participates in dry runs and writes with the same
permission-preserving atomic replacement as structured file-value updates.

A complete release task typically combines it with structured updates:

```drun
task "prepare-release" means "Prepare and verify a developer-selected release version":
requires $version as string matching semver_optional_v

set $release_version to "{$version without prefix 'v'}"

update json "/version" in "package.json" to "{$release_version}" or fail
promote changelog "CHANGELOG.md" to version "{$release_version}"
```

The complete executable example is
[`examples/75-changelog-promotion.drun`](https://github.com/phillarmonic/drun/blob/master/examples/75-changelog-promotion.drun).

#### File Inspection

```drun
Expand Down Expand Up @@ -858,6 +906,38 @@ task "download_config":
allow permissions ["read","write"] to ["user","group","others"]
```

#### Pausing Execution (Fixed Waits)

Pause task execution for a fixed amount of time before continuing with the
next statement. Durations can be given in seconds, minutes, or hours
(singular and plural forms are interchangeable), and the amount can be a
number literal or an interpolated variable.

```drun
wait 5 seconds
wait 1 minute
wait 2 hours

# Variable-driven durations, e.g. exponential-style backoff
let $backoff = "30"
wait {$backoff} seconds

# Retry loop with a growing pause between attempts
for each $attempt in ["1", "2", "3"]:
try:
run "./flaky-deploy.sh"
break
catch:
warn "Attempt {$attempt} failed, backing off"
wait {$attempt} minutes
```

Fractional values are supported (e.g. `wait 0.5 seconds`, `wait 1.5 minutes`).
In dry-run mode the wait is reported but not performed.

> **Note:** To wait until a service responds rather than for a fixed amount of
> time, use `wait for service at ... to be ready` (see below).

#### Network Health Checks and Service Waiting

```drun
Expand Down
12 changes: 12 additions & 0 deletions docs/docs/reference/language/grammar-and-lexical-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ that operate on the project declaration in the currently executing Drun file.
They honor custom `--file` paths and use the same dry-run and atomic-write
guarantees as other structured file-value statements.

### Changelog statements

```ebnf
changelog-promote = "promote", "changelog", string, "to", "version", string,
["on", string] ;
```

The first string is the changelog file path, the second is the release
version, and the optional `on` string is a `YYYY-MM-DD` release date. See
[Changelog promotion](built-in-actions.md#changelog-promotion) for the exact
transformation and failure modes.

### Composite Git version guard

```ebnf
Expand Down
21 changes: 21 additions & 0 deletions docs/docs/reference/language/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -768,4 +768,25 @@ finally:
cleanup temporary resources
```

### Wait Statements

Pause execution for a fixed duration before continuing:

```drun
wait <number> <unit>
wait {<variable>} <unit>
```

Where `<unit>` is `second`/`seconds`, `minute`/`minutes`, or `hour`/`hours`.

```drun
wait 5 seconds
wait 1 minute
wait {$backoff} seconds
```

To wait until a service responds instead of for a fixed duration, see
`wait for service at "url" to be ready` in
[Built-in Actions](./built-in-actions.md#network-health-checks-and-service-waiting).

---
16 changes: 7 additions & 9 deletions docs/docs/reference/language/types-and-control-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,20 +301,18 @@ run in parallel:
wait for all to complete
```

#### Range Iteration
#### Retry with Backoff

```drun
for port from 3000 to 3005:
check if port {port} is available
Pause between attempts with a fixed `wait` (seconds, minutes, or hours):

for i from 1 to retry_count:
```drun
for each $attempt in ["1", "2", "3"]:
try:
perform operation
run "./flaky-deploy.sh"
break
catch:
if i == retry_count:
fail "Max retries exceeded"
wait {i} seconds
warn "Attempt {$attempt} failed, backing off"
wait {$attempt} seconds
```

#### Filtered Iteration
Expand Down
Loading
Loading