Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
217 changes: 177 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
# Time Resource

Implements a resource that reports new versions on a configured interval. The
interval can be arbitrarily long.
Implements a resource that reports new versions on a configured interval or cron schedule. The time intervals can be
arbitrarily long.

<a href="https://ci.concourse-ci.org/teams/main/pipelines/resource/jobs/build?vars.type=%22time%22">
<img src="https://ci.concourse-ci.org/api/v1/teams/main/pipelines/resource/jobs/build/badge?vars.type=%22time%22" alt="Build Status">
</a>

This resource is built to satisfy "trigger this build at least once every 5
minutes," not "trigger this build on the 10th hour of every Sunday." That
level of precision is better left to other tools.
This resource is built to satisfy needs like "trigger this build at least once every 5 minutes" or "trigger this build
at specific times using cron expressions." For simple interval-based triggering, the interval configuration is simpler
to use. For more complex scheduling, the cron configuration provides greater flexibility.

## Source Configuration

### Interval-based Configuration

* `interval`: *Optional.* The interval on which to report new versions. Valid
units are: “ns”, “us” (or “µs”), “ms”, “s”, “m”, “h”. Examples: `60s`, `90m`,
`1h30m`. If not specified, this resource will generate exactly 1 new version
per calendar day on each of the valid `days`.
units are: "s", "m", "h". Examples: `60s`, `90m`, `1h30m`. If not specified, this resource will
generate exactly 1 new version per calendar day on each of the valid `days`.

* `location`: *Optional. Default `UTC`.* The
[location](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) in
Expand Down Expand Up @@ -70,31 +71,82 @@ level of precision is better left to other tools.
These can be combined to emit a new version on an interval during a particular
time period.

* `initial_version`: *Optional.* When using `start` and `stop` as a trigger for
a job, you will be unable to run the job manually until it goes into the
configured time range for the first time (manual runs will work once the `time`
resource has produced it's first version).
### Cron-based Configuration

* `cron`: *Optional.* A cron expression that defines when new versions should be created. Standard cron format is
supported with 5 fields (minute, hour, day of month, month, day of week).

**Important:** The 6-field format (including seconds) is not supported. You must use the standard 5-field format that
only specifies minute precision.

e.g.

```
cron: "0 * * * *" # Every hour at minute 0
```

**Tags:** Shorthand aliases for common schedules:

| Tag | Expression | Schedule |
|-----|------------|----------|
| `@yearly` / `@annually` | `0 0 1 1 *` | Midnight, Jan 1st |
| `@monthly` | `0 0 1 * *` | Midnight, 1st of month |
| `@weekly` | `0 0 * * 0` | Midnight, Sunday |
| `@daily` | `0 0 * * *` | Midnight |
| `@hourly` | `0 * * * *` | Start of every hour |
| `@30minutes` | `0,30 * * * *` | Every 30 minutes (:00, :30) |
| `@15minutes` | `*/15 * * * *` | Every 15 minutes |
| `@10minutes` | `*/10 * * * *` | Every 10 minutes |
| `@5minutes` | `*/5 * * * *` | Every 5 minutes |

e.g.
```
cron: "@daily" # Run once a day at midnight
```

**Modifiers:** Special modifiers for complex scheduling:

| Field | Modifier | Example | Description |
|-------|----------|---------|-------------|
| Day of Month | `L` | `0 2 L * *` | Last day of month (e.g., 28th/29th/30th/31st) |
| | `W` | `0 1 15W * *` | Nearest weekday to date (if 15th is Sat → Fri 14th) |
| | `LW` | `0 2 LW * *` | Last weekday of month |
| Day of Week | `L` | `0 3 * * 5L` | Last occurrence in month (5L = last Friday) |
| | `#` | `0 5 * * 1#1` | Nth occurrence in month (2#1 = first Monday) |
```

**Note: You cannot use `cron` together with `interval`, `start`, `stop`, or `days`. Use either the cron-based or
interval-based configuration.**

* `location`: *Optional. Default `UTC`.* When used with `cron`, the cron schedule is evaluated in this timezone.
See interval-based configuration above for format details.

### Common Configuration Options

* `initial_version`: *Optional.* When using `start` and `stop` or `cron` as a trigger for
a job, you will be unable to run the job manually until it reaches the
configured time range or cron schedule for the first time (manual runs will work once the `time`
resource has produced its first version).

To get around this issue, there are two approaches:
* Use `initial_version: true`, which will produce a new version that is
set to the current time, if `check` runs and there isn't already a version
specified. **NOTE: This has a downside that if used with `trigger: true`, it will
kick off the correlating job when the pipeline is first created, even
outside of the specified window**.
* Alternatively, once you push a pipeline that utilizes `start` and `stop`, run the
following fly command to run the resource check from a previous point
in time (see [this issue](https://github.com/concourse/time-resource/issues/24#issuecomment-689422764)
for 6.x.x+ or [this issue](https://github.com/concourse/time-resource/issues/11#issuecomment-562385742)
for older Concourse versions).

```
fly -t <your target> \
check-resource --resource <pipeline>/<your resource>
--from "time:2000-01-01T00:00:00Z" # the important part
```

This has the benefit that it shouldn't trigger that initial job run, but
will still allow you to manually run the job if needed.
* Use `initial_version: true`, which will produce a new version that is
set to the current time, if `check` runs and there isn't already a version
specified. **NOTE: This has a downside that if used with `trigger: true`, it will
kick off the correlating job when the pipeline is first created, even
outside of the specified window**.
* Alternatively, once you push a pipeline that utilizes time-based constraints, run the
following fly command to run the resource check from a previous point
in time (see [this issue](https://github.com/concourse/time-resource/issues/24#issuecomment-689422764)
for 6.x.x+ or [this issue](https://github.com/concourse/time-resource/issues/11#issuecomment-562385742)
for older Concourse versions).

```
fly -t <your target> \
check-resource --resource <pipeline>/<your resource>
--from "time:2000-01-01T00:00:00Z" # the important part
```
This has the benefit that it shouldn't trigger that initial job run, but
will still allow you to manually run the job if needed.

e.g.

Expand All @@ -118,24 +170,22 @@ level of precision is better left to other tools.
```
## Behavior

### `check`: Produce timestamps satisfying the interval.
### `check`: Produce timestamps satisfying the interval or cron schedule.

Returns current version and new version only if it has been longer than `interval` since the
given version, or if there is no version given.

given version, or if the time matches the specified cron expression, or if there is no version given.

### `in`: Report the given time.

Fetches the given timestamp. Creates three files:
1. `input` which contains the request provided by Concourse
1. `timestamp` which contains the fetched version in the following format: `2006-01-02 15:04:05.999999999 -0700 MST`
1. `epoch` which contains the fetched version as a Unix epoch Timestamp (integer only)
2. `timestamp` which contains the fetched version in the following format: `2006-01-02 15:04:05.999999999 -0700 MST`
3. `epoch` which contains the fetched version as a Unix epoch Timestamp (integer only)

#### Parameters

*None.*


### `out`: Produce the current time.

Returns a version for the current timestamp. This can be used to record the
Expand All @@ -145,7 +195,6 @@ time within a build plan, e.g. after running some long-running task.

*None.*


## Examples

### Periodic trigger
Expand All @@ -165,6 +214,40 @@ jobs:
config: # ...
```

### Cron trigger

```yaml
resources:
- name: nightly-build
type: time
source:
cron: "0 0 * * *" # Every day at midnight

jobs:
- name: run-nightly-build
plan:
- get: nightly-build
trigger: true
- task: build
config: # ...
```
### Cron trigger using tags
```yaml
resources:
- name: weekly-cleanup
type: time
source:
cron: "@weekly" # Every Sunday at midnight

jobs:
- name: run-weekly-cleanup
plan:
- get: weekly-cleanup
trigger: true
- task: cleanup
config: # ...
```

### Trigger once within time range

```yaml
Expand Down Expand Up @@ -225,13 +308,67 @@ jobs:
config: # ...
```

### Trigger only on specific days
```yaml
resources:
- name: weekday-mornings
type: time
source:
interval: 1h
start: 9:00 AM
stop: 12:00 PM
days: [Monday, Tuesday, Wednesday, Thursday, Friday]
location: Europe/London

jobs:
- name: something-every-hour-on-weekday-mornings
plan:
- get: weekday-mornings
trigger: true
- task: something
config: # ...
```
### Cron trigger with modifiers
```yaml
resources:
- name: last-day-of-month
type: time
source:
cron: "0 9 L * *" # 9:00 AM on the last day of each month
location: America/New_York

jobs:
- name: monthly-report
plan:
- get: last-day-of-month
trigger: true
- task: generate-report
config: # ...
```
```yaml
resources:
- name: second-monday
type: time
source:
cron: "0 7 * * 1#2" # 7:00 AM on the second Monday of each month
location: Europe/Berlin

jobs:
- name: bi-monthly-planning
plan:
- get: second-monday
trigger: true
- task: planning-meeting
config: # ...
```

## Development

### Prerequisites

* golang is *required* - version 1.9.x is tested; earlier versions may also
* golang is *required* - version 1.25.x is tested; earlier versions may also
work.
* docker is *required* - version 17.06.x is tested; earlier versions may also
* docker is *required* - version 25.x is tested; earlier versions may also
work.
* go mod is used for dependency management of the golang packages.

Expand Down
28 changes: 26 additions & 2 deletions check_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,43 @@ func (*CheckCommand) Run(request models.CheckRequest) ([]models.Version, error)
Interval: request.Source.Interval,
Days: request.Source.Days,
StartAfter: request.Source.StartAfter,
Cron: request.Source.Cron,
}

var versions []models.Version

if !previousTime.IsZero() {
versions = append(versions, models.Version{Time: previousTime})
} else if request.Source.InitialVersion {
versions = append(versions, models.Version{Time: currentTime})
// For cron with initial_version, use the cron boundary time for consistency.
// For non-cron, use currentTime (original behavior).
versionTime := currentTime
if request.Source.Cron != nil {
cronTime := tl.Latest(currentTime)
if !cronTime.IsZero() {
versionTime = cronTime
}
}
versions = append(versions, models.Version{Time: versionTime})
return versions, nil
}

if tl.Check(currentTime) {
versions = append(versions, models.Version{Time: currentTime})
var versionTime time.Time

// For cron expressions, use the actual scheduled cron time
// instead of the check time. This ensures versions are at cron boundaries.
// Example: cron @5minutes, check at 3:07pm → version time = 3:05pm
if request.Source.Cron != nil {
versionTime = tl.Latest(currentTime)
}

// For non-cron (interval, start/stop ranges), use currentTime
if versionTime.IsZero() {
versionTime = currentTime
}

versions = append(versions, models.Version{Time: versionTime})
}

return versions, nil
Expand Down
Loading