cronv-rb 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ai-fix.yml +61 -0
- data/.github/workflows/ai-review.yml +63 -0
- data/.github/workflows/ai-triage.yml +61 -0
- data/.github/workflows/auto-merge.yml +82 -0
- data/.github/workflows/codeql.yml +29 -0
- data/.github/workflows/compare_cronv_outputs.yml +67 -0
- data/.github/workflows/release-please.yml +132 -0
- data/.github/workflows/rspec.yml +32 -0
- data/.github/workflows/rubocop.yml +32 -0
- data/.gitignore +15 -0
- data/.release-please-manifest.json +3 -0
- data/.rspec +3 -0
- data/.rubocop.yml +11 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +46 -0
- data/Gemfile +14 -0
- data/LICENSE +21 -0
- data/README.md +72 -0
- data/Rakefile +8 -0
- data/bin/console +18 -0
- data/bin/setup +8 -0
- data/cronv-rb.gemspec +44 -0
- data/docs/release.md +136 -0
- data/exe/cronv +8 -0
- data/fixtures/sample.crontab +80 -0
- data/lib/cronv_rb/crontab.rb +98 -0
- data/lib/cronv_rb/logger.rb +25 -0
- data/lib/cronv_rb/main.rb +34 -0
- data/lib/cronv_rb/option.rb +61 -0
- data/lib/cronv_rb/record.rb +51 -0
- data/lib/cronv_rb/schedule.rb +25 -0
- data/lib/cronv_rb/template.rb +152 -0
- data/lib/cronv_rb/version.rb +5 -0
- data/lib/cronv_rb/visualizer.rb +54 -0
- data/lib/cronv_rb.rb +12 -0
- data/release-please-config.json +20 -0
- data/renovate.json +43 -0
- data/scripts/compare_cronv_outputs.rb +140 -0
- metadata +125 -0
data/Gemfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
|
|
5
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
|
6
|
+
|
|
7
|
+
# Specify your gem's dependencies in cronv-rb.gemspec
|
|
8
|
+
gemspec
|
|
9
|
+
|
|
10
|
+
gem 'fugit', '~> 1.2'
|
|
11
|
+
gem 'logger'
|
|
12
|
+
gem 'rspec', '~> 3.0'
|
|
13
|
+
gem 'rubocop'
|
|
14
|
+
gem 'rubocop-rspec'
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Masato Yamashita
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# cronv-rb
|
|
2
|
+
|
|
3
|
+
A Ruby implementation of [cronv](https://github.com/takumakanari/cronv) (originally written in Go) that visualizes cron schedules from crontab as an interactive HTML timeline.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'cronv-rb'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
$ bundle install
|
|
16
|
+
|
|
17
|
+
Or install it yourself as:
|
|
18
|
+
|
|
19
|
+
$ gem install cronv-rb
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Pipe your crontab into `cronv` to generate an HTML timeline:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
crontab -l | bundle exec ruby exe/cronv -o crontab.html
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Options
|
|
30
|
+
|
|
31
|
+
| Option | Description | Default |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `-o`, `--output PATH` | Path to output HTML file | `./crontab.html` |
|
|
34
|
+
| `-d`, `--duration DURATION` | Duration to visualize (`1d` / `6h` / `30m`) | `6h` |
|
|
35
|
+
| `--from-date DATE` | Start date (`YYYY/MM/DD`) | Current date (UTC) |
|
|
36
|
+
| `--from-time TIME` | Start time (`HH:MM`) | Current time (UTC) |
|
|
37
|
+
| `-t`, `--title TITLE` | Title label of output | `Cron Tasks` |
|
|
38
|
+
| `-w`, `--width WIDTH` | Table width of output | `100` |
|
|
39
|
+
|
|
40
|
+
### Examples
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
# Visualize the next 24 hours starting from a specific date
|
|
44
|
+
crontab -l | bundle exec ruby exe/cronv \
|
|
45
|
+
-o crontab.html \
|
|
46
|
+
--from-date 2025/01/01 \
|
|
47
|
+
--from-time 00:00 \
|
|
48
|
+
-d 1d
|
|
49
|
+
|
|
50
|
+
# Visualize the next 30 minutes with a custom title
|
|
51
|
+
crontab -l | bundle exec ruby exe/cronv \
|
|
52
|
+
-o crontab.html \
|
|
53
|
+
-d 30m \
|
|
54
|
+
-t "My Cron Jobs"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
bundle install
|
|
63
|
+
bundle exec rspec
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Contributing
|
|
67
|
+
|
|
68
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/M-Yamashita01/cronv-rb.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Layout/LeadingCommentSpace
|
|
4
|
+
#!/usr/bin/env ruby
|
|
5
|
+
# rubocop:enable Layout/LeadingCommentSpace
|
|
6
|
+
|
|
7
|
+
require 'bundler/setup'
|
|
8
|
+
require 'cronv/rb'
|
|
9
|
+
|
|
10
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
11
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
12
|
+
|
|
13
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
14
|
+
# require "pry"
|
|
15
|
+
# Pry.start
|
|
16
|
+
|
|
17
|
+
require 'irb'
|
|
18
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/cronv-rb.gemspec
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
require 'cronv_rb/version'
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.name = 'cronv-rb'
|
|
9
|
+
spec.version = CronvRb::VERSION
|
|
10
|
+
spec.authors = ['Masato Yamashita']
|
|
11
|
+
spec.email = ['4653960+M-Yamashita01@users.noreply.github.com']
|
|
12
|
+
|
|
13
|
+
spec.summary = 'Visualize your cron schedules in crontab by Ruby'
|
|
14
|
+
spec.description = 'A gem that rewrites cronv, originally written in Go, in Ruby to visualize your cron schedules in crontab.'
|
|
15
|
+
spec.homepage = 'https://github.com/M-Yamashita01/cronv-rb'
|
|
16
|
+
spec.license = 'MIT'
|
|
17
|
+
|
|
18
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
|
19
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
|
20
|
+
if spec.respond_to?(:metadata)
|
|
21
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
|
22
|
+
|
|
23
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
24
|
+
spec.metadata['source_code_uri'] = 'https://github.com/M-Yamashita01/cronv-rb'
|
|
25
|
+
spec.metadata['changelog_uri'] = 'https://github.com/M-Yamashita01/cronv-rb'
|
|
26
|
+
else
|
|
27
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
|
28
|
+
'public gem pushes.'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
spec.required_ruby_version = '>= 3.2'
|
|
32
|
+
# Specify which files should be added to the gem when it is released.
|
|
33
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
34
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
35
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
36
|
+
end
|
|
37
|
+
spec.bindir = 'exe'
|
|
38
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
39
|
+
spec.require_paths = ['lib']
|
|
40
|
+
|
|
41
|
+
spec.add_development_dependency 'bundler', '>= 2.5'
|
|
42
|
+
spec.add_development_dependency 'rake', '~> 13.4'
|
|
43
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
44
|
+
end
|
data/docs/release.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Release Flow
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This project uses an automated release flow driven by [release-please](https://github.com/googleapis/release-please)
|
|
6
|
+
and Conventional Commits. There is no manual tagging.
|
|
7
|
+
|
|
8
|
+
The pipeline is:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
Conventional Commits merged to main
|
|
12
|
+
|
|
|
13
|
+
v
|
|
14
|
+
release-please keeps a Release PR up to date (version bump + CHANGELOG)
|
|
15
|
+
|
|
|
16
|
+
v (the Release PR is merged: see "How the Release PR is merged" below)
|
|
17
|
+
release-please creates the tag and the GitHub Release
|
|
18
|
+
|
|
|
19
|
+
v
|
|
20
|
+
the publish job builds the gem and pushes it to RubyGems (Trusted Publishing)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How the Release PR is merged
|
|
24
|
+
|
|
25
|
+
Patch releases ship automatically; larger releases need a human.
|
|
26
|
+
|
|
27
|
+
- Patch bump (`x.y.Z`): the `auto-merge-patch` job runs weekly (Mondays 00:00 UTC
|
|
28
|
+
/ 09:00 JST) and merges the open Release PR automatically when it is a patch-only
|
|
29
|
+
bump. This ships accumulated `fix:` / `fix(deps):` changes without manual action.
|
|
30
|
+
You can also trigger it on demand from the Actions tab (Run workflow).
|
|
31
|
+
- Minor bump (`x.Y.0`, from `feat:`) or major bump: the Release PR is left open for
|
|
32
|
+
a human to review and merge, since these are larger changes.
|
|
33
|
+
|
|
34
|
+
## Conventional Commits
|
|
35
|
+
|
|
36
|
+
release-please derives the next version from commit messages, so the prefix matters:
|
|
37
|
+
|
|
38
|
+
| Prefix | Effect on version (while in 0.x) |
|
|
39
|
+
|--------|----------------------------------|
|
|
40
|
+
| `feat:` | MINOR bump (new feature); shown in release notes |
|
|
41
|
+
| `fix:` | PATCH bump (bug fix); shown in release notes |
|
|
42
|
+
| `fix(deps):` | PATCH bump (dependency update, used by Renovate); shown in release notes |
|
|
43
|
+
| `perf:` | PATCH bump (performance improvement); shown in release notes |
|
|
44
|
+
| `feat!:` / `BREAKING CHANGE:` | MINOR bump (breaking changes stay MINOR until 1.0.0) |
|
|
45
|
+
| `docs:`, `refactor:`, `chore:` | No version bump; hidden from release notes |
|
|
46
|
+
|
|
47
|
+
Note on how release-please decides the bump: internally, only `feat` and
|
|
48
|
+
breaking changes map to MINOR/MAJOR; every other releasable type maps to PATCH.
|
|
49
|
+
To keep `docs:` / `refactor:` / `chore:` from cutting a pointless release, they
|
|
50
|
+
are marked hidden in `release-please-config.json`, which also removes them from
|
|
51
|
+
the release notes. There is no release-please setting that both shows a type in
|
|
52
|
+
the notes and skips the version bump; that is why these types are hidden entirely.
|
|
53
|
+
|
|
54
|
+
If nothing since the last release bumps the version, no Release PR is created,
|
|
55
|
+
so weeks with only `docs:` / `refactor:` / `chore:` changes produce no release.
|
|
56
|
+
|
|
57
|
+
## Releasing
|
|
58
|
+
|
|
59
|
+
### 1. Merge feature/fix PRs to main as usual
|
|
60
|
+
|
|
61
|
+
Each PR should use a Conventional Commit title. When such commits land on `main`,
|
|
62
|
+
the `Release Please` workflow opens or updates a single Release PR titled
|
|
63
|
+
`chore(main): release x.y.z`.
|
|
64
|
+
|
|
65
|
+
### 2. The Release PR is merged
|
|
66
|
+
|
|
67
|
+
- Patch-only Release PRs are merged automatically on the weekly schedule (see
|
|
68
|
+
"How the Release PR is merged" above). No action is needed.
|
|
69
|
+
- Minor/major Release PRs wait for a human. Open the PR, check the proposed
|
|
70
|
+
version and the generated `CHANGELOG.md` entry, then merge when ready.
|
|
71
|
+
|
|
72
|
+
### 3. Merging the Release PR triggers the release
|
|
73
|
+
|
|
74
|
+
Whichever way it is merged, it:
|
|
75
|
+
|
|
76
|
+
1. Updates `CHANGELOG.md`, `lib/cronv_rb/version.rb`, and
|
|
77
|
+
`.release-please-manifest.json` on `main`
|
|
78
|
+
2. Creates the git tag (e.g. `v0.1.1`)
|
|
79
|
+
3. Creates the GitHub Release with the changelog as its body
|
|
80
|
+
|
|
81
|
+
### 4. The gem is published automatically
|
|
82
|
+
|
|
83
|
+
The `publish` job in the same workflow runs when a release was created
|
|
84
|
+
(`releases_created == 'true'`). It builds the gem and pushes it to
|
|
85
|
+
[RubyGems](https://rubygems.org/gems/cronv-rb) using Trusted Publishing (OIDC),
|
|
86
|
+
so no API key is stored in the repository.
|
|
87
|
+
|
|
88
|
+
If a publish needs to be re-run or a version needs to be backfilled, trigger the
|
|
89
|
+
workflow manually from the Actions tab (Run workflow) with `force_publish`
|
|
90
|
+
checked. This builds and pushes whatever version is currently on `main`.
|
|
91
|
+
|
|
92
|
+
### 5. Verify the release
|
|
93
|
+
|
|
94
|
+
- Check the [Releases page](https://github.com/M-Yamashita01/cronv-rb/releases)
|
|
95
|
+
- Confirm the new version is live on [RubyGems](https://rubygems.org/gems/cronv-rb)
|
|
96
|
+
- Confirm the changelog reads correctly
|
|
97
|
+
|
|
98
|
+
## Release Infrastructure
|
|
99
|
+
|
|
100
|
+
| File | Purpose |
|
|
101
|
+
|------|---------|
|
|
102
|
+
| `.github/workflows/release-please.yml` | Maintains the Release PR on push to `main`; auto-merges patch-only Release PRs weekly; publishes the gem when a release is created |
|
|
103
|
+
| `release-please-config.json` | release-please settings (release type, version bump rules, changelog sections) |
|
|
104
|
+
| `.release-please-manifest.json` | Tracks the current released version |
|
|
105
|
+
| `lib/cronv_rb/version.rb` | The `CronvRb::VERSION` constant; the source of truth for the version, updated by release-please |
|
|
106
|
+
|
|
107
|
+
## RubyGems Trusted Publishing
|
|
108
|
+
|
|
109
|
+
Publishing uses RubyGems Trusted Publishing (OIDC) instead of a stored API key.
|
|
110
|
+
A one-time setup is required on RubyGems.org (Profile > Trusted Publishers):
|
|
111
|
+
|
|
112
|
+
- Repository: `M-Yamashita01/cronv-rb`
|
|
113
|
+
- Workflow filename: `release-please.yml`
|
|
114
|
+
- Gem name: `cronv-rb`
|
|
115
|
+
|
|
116
|
+
Because the gem is not yet registered, create it as a pending trusted publisher
|
|
117
|
+
so the first OIDC push both creates the gem and registers ownership.
|
|
118
|
+
|
|
119
|
+
## Renovate
|
|
120
|
+
|
|
121
|
+
Renovate is configured to use `fix(deps):` commit messages (see `renovate.json`),
|
|
122
|
+
so dependency updates become PATCH-level entries in the Release PR. This lets a
|
|
123
|
+
weekly dependency refresh ship as a single patch release when the Release PR is
|
|
124
|
+
merged.
|
|
125
|
+
|
|
126
|
+
## Fixing a Bad Release
|
|
127
|
+
|
|
128
|
+
Prefer rolling forward: fix the issue on `main` and let the next Release PR ship a
|
|
129
|
+
new patch version (e.g. `v0.1.2`).
|
|
130
|
+
|
|
131
|
+
If a release must be pulled:
|
|
132
|
+
|
|
133
|
+
1. Delete the release from the GitHub Releases page
|
|
134
|
+
2. Delete the tag: `git push origin :refs/tags/v0.1.1`
|
|
135
|
+
3. Yank the gem from RubyGems if it was published: `gem yank cronv-rb -v 0.1.1`
|
|
136
|
+
4. Fix the issue on `main` and ship a new patch via the normal flow
|
data/exe/cronv
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Sample crontab for comparing output between cronv-rb and the OSS cronv
|
|
2
|
+
# https://github.com/takumakanari/cronv
|
|
3
|
+
|
|
4
|
+
# --- Basic examples ---
|
|
5
|
+
|
|
6
|
+
# Every minute
|
|
7
|
+
* * * * * /usr/local/bin/every_minute.sh
|
|
8
|
+
|
|
9
|
+
# Every 5 minutes
|
|
10
|
+
*/5 * * * * /usr/local/bin/every_5_minutes.sh
|
|
11
|
+
|
|
12
|
+
# Every 10 minutes
|
|
13
|
+
*/10 * * * * /usr/local/bin/every_10_minutes.sh
|
|
14
|
+
|
|
15
|
+
# Every 15 minutes
|
|
16
|
+
*/15 * * * * /usr/local/bin/every_15_minutes.sh
|
|
17
|
+
|
|
18
|
+
# Every 30 minutes
|
|
19
|
+
*/30 * * * * /usr/local/bin/every_30_minutes.sh
|
|
20
|
+
|
|
21
|
+
# --- Hourly / Daily ---
|
|
22
|
+
|
|
23
|
+
# Every hour at minute 0
|
|
24
|
+
0 * * * * /usr/local/bin/hourly.sh
|
|
25
|
+
|
|
26
|
+
# Every day at midnight
|
|
27
|
+
0 0 * * * /usr/local/bin/daily_midnight.sh
|
|
28
|
+
|
|
29
|
+
# Every day at noon
|
|
30
|
+
0 12 * * * /usr/local/bin/daily_noon.sh
|
|
31
|
+
|
|
32
|
+
# Every day at 6 AM and 6 PM
|
|
33
|
+
0 6,18 * * * /usr/local/bin/twice_daily.sh
|
|
34
|
+
|
|
35
|
+
# --- Weekly ---
|
|
36
|
+
|
|
37
|
+
# Every Monday at 9 AM
|
|
38
|
+
0 9 * * 1 /usr/local/bin/weekly_monday.sh
|
|
39
|
+
|
|
40
|
+
# Every weekday (Mon-Fri) at 8 AM
|
|
41
|
+
0 8 * * 1-5 /usr/local/bin/weekday_morning.sh
|
|
42
|
+
|
|
43
|
+
# Every weekend (Sat-Sun) at 10 AM
|
|
44
|
+
0 10 * * 6,0 /usr/local/bin/weekend_morning.sh
|
|
45
|
+
|
|
46
|
+
# --- Monthly ---
|
|
47
|
+
|
|
48
|
+
# First day of every month at midnight
|
|
49
|
+
0 0 1 * * /usr/local/bin/monthly_first.sh
|
|
50
|
+
|
|
51
|
+
# Last possible day check: 28th of every month at 2 AM
|
|
52
|
+
0 2 28 * * /usr/local/bin/monthly_28th.sh
|
|
53
|
+
|
|
54
|
+
# --- Specific months ---
|
|
55
|
+
|
|
56
|
+
# Every day in January at midnight
|
|
57
|
+
0 0 * 1 * /usr/local/bin/january_daily.sh
|
|
58
|
+
|
|
59
|
+
# Every day in July and December at noon
|
|
60
|
+
0 12 * 7,12 * /usr/local/bin/july_december_noon.sh
|
|
61
|
+
|
|
62
|
+
# --- Step and range combinations ---
|
|
63
|
+
|
|
64
|
+
# Every 2 hours
|
|
65
|
+
0 */2 * * * /usr/local/bin/every_2_hours.sh
|
|
66
|
+
|
|
67
|
+
# Every 3 hours between 6 AM and 21 PM
|
|
68
|
+
0 6-21/3 * * * /usr/local/bin/daytime_every_3_hours.sh
|
|
69
|
+
|
|
70
|
+
# Minutes 0, 15, 30, 45 of every hour
|
|
71
|
+
0,15,30,45 * * * * /usr/local/bin/quarter_hour.sh
|
|
72
|
+
|
|
73
|
+
# --- @ aliases ---
|
|
74
|
+
|
|
75
|
+
@hourly /usr/local/bin/at_hourly.sh
|
|
76
|
+
@daily /usr/local/bin/at_daily.sh
|
|
77
|
+
@weekly /usr/local/bin/at_weekly.sh
|
|
78
|
+
@monthly /usr/local/bin/at_monthly.sh
|
|
79
|
+
@yearly /usr/local/bin/at_yearly.sh
|
|
80
|
+
@reboot /usr/local/bin/at_reboot.sh
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CronvRb
|
|
4
|
+
class Extra
|
|
5
|
+
attr_reader :line, :label, :job
|
|
6
|
+
|
|
7
|
+
def initialize(line:, label:, job:)
|
|
8
|
+
@line = line
|
|
9
|
+
@label = label
|
|
10
|
+
@job = job
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Crontab
|
|
15
|
+
attr_reader :line, :schedule, :job
|
|
16
|
+
|
|
17
|
+
def initialize(line:, schedule:, job:)
|
|
18
|
+
@line = line
|
|
19
|
+
@schedule = schedule
|
|
20
|
+
@job = job
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# rubocop:disable Metrics/AbcSize
|
|
25
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
|
26
|
+
# rubocop:disable Style/OptionalArguments
|
|
27
|
+
# return [crontab, extra]
|
|
28
|
+
def parse(context = {}, line)
|
|
29
|
+
parts = line.split
|
|
30
|
+
schedule = nil
|
|
31
|
+
job = []
|
|
32
|
+
|
|
33
|
+
if parts[0].start_with?('@')
|
|
34
|
+
raise ArgumentError, "invalid crontab line: '#{line}'" if parts.length < 2
|
|
35
|
+
|
|
36
|
+
if parts[0] == '@reboot'
|
|
37
|
+
return [nil,
|
|
38
|
+
Extra.new(
|
|
39
|
+
line: line,
|
|
40
|
+
label: parts[0],
|
|
41
|
+
job: parts[1..].join(' ')
|
|
42
|
+
)]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
schedule = Schedule.new(
|
|
46
|
+
minutes: nil,
|
|
47
|
+
hour: nil,
|
|
48
|
+
day_of_month: nil,
|
|
49
|
+
month: nil,
|
|
50
|
+
day_of_week: nil,
|
|
51
|
+
year: nil,
|
|
52
|
+
schedule_alias: parts[0]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return [Crontab.new(
|
|
56
|
+
line: line,
|
|
57
|
+
schedule: schedule,
|
|
58
|
+
job: parts[1..].join(' ')
|
|
59
|
+
), nil]
|
|
60
|
+
else
|
|
61
|
+
raise ArgumentError, "invalid crontab line: '#{line}'" if parts.length < 5
|
|
62
|
+
|
|
63
|
+
schedule = Schedule.new(
|
|
64
|
+
minutes: parts[0],
|
|
65
|
+
hour: parts[1],
|
|
66
|
+
day_of_month: parts[2],
|
|
67
|
+
month: parts[3],
|
|
68
|
+
day_of_week: parts[4],
|
|
69
|
+
year: nil,
|
|
70
|
+
schedule_alias: nil
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
job = parts[5..].join(' ')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
[Crontab.new(
|
|
77
|
+
line: line,
|
|
78
|
+
schedule: schedule,
|
|
79
|
+
job: job
|
|
80
|
+
), nil]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
# rubocop:enable Metrics/AbcSize
|
|
84
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
85
|
+
# rubocop:enable Style/OptionalArguments
|
|
86
|
+
|
|
87
|
+
def running_every_minutes?(schedule)
|
|
88
|
+
fields = schedule.to_crontab.split
|
|
89
|
+
fields.each_with_index.all? do |field, i|
|
|
90
|
+
if i.zero?
|
|
91
|
+
%w[* */1].include?(field)
|
|
92
|
+
else
|
|
93
|
+
field == '*'
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
class Logger
|
|
6
|
+
class << self
|
|
7
|
+
def info(message)
|
|
8
|
+
info_logger.info(message)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def error(message)
|
|
12
|
+
error_logger.error(message)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def info_logger
|
|
18
|
+
@info_logger ||= Logger.new($stdout)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def error_logger
|
|
22
|
+
@error_logger ||= Logger.new($stderr)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module CronvRb
|
|
6
|
+
# rubocop:disable Metrics/AbcSize
|
|
7
|
+
def self.main
|
|
8
|
+
option = Option.new_cronv_option(Time.now)
|
|
9
|
+
|
|
10
|
+
OptionParser.new do |opt|
|
|
11
|
+
opt.on('-o', '--output PATH', 'path to .html file to output') { |v| option.output_file_path = v }
|
|
12
|
+
opt.on('-d', '--duration DURATION', 'duration to visualize in N{suffix} style. e.g.) 1d(day)/1h(hour)/1m(minute)') { |v| option.duration = v }
|
|
13
|
+
opt.on('--from-date DATE', "start date in the format '#{Option::OPT_DATE_FORMAT}' to visualize") { |v| option.from_date = v }
|
|
14
|
+
opt.on('--from-time TIME', "start time in the format '#{Option::OPT_TIME_FORMAT}' to visualize") { |v| option.from_time = v }
|
|
15
|
+
opt.on('-t', '--title TITLE', 'title/label of output') { |v| option.title = v }
|
|
16
|
+
opt.on('-w', '--width WIDTH', 'Table width of output') { |v| option.width = v.to_i }
|
|
17
|
+
opt.on('-v', '--version', 'Show version') do
|
|
18
|
+
puts "cronv-rb #{VERSION}"
|
|
19
|
+
exit
|
|
20
|
+
end
|
|
21
|
+
end.parse!
|
|
22
|
+
|
|
23
|
+
visualizer = Visualizer.new_visualizer(option)
|
|
24
|
+
|
|
25
|
+
$stdin.each_line do |line|
|
|
26
|
+
visualizer.add(line)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
path = visualizer.dump
|
|
30
|
+
puts "[#{option.title}] #{visualizer.records.size} tasks."
|
|
31
|
+
puts "[#{option.title}] '#{path}' generated."
|
|
32
|
+
end
|
|
33
|
+
# rubocop:enable Metrics/AbcSize
|
|
34
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CronvRb
|
|
4
|
+
class Option
|
|
5
|
+
OPT_DATE_FORMAT = '%Y/%m/%d'
|
|
6
|
+
OPT_TIME_FORMAT = '%H:%M'
|
|
7
|
+
OPT_DEFAULT_DURATION = '6h'
|
|
8
|
+
OPT_DEFAULT_OUTPUT_PATH = './crontab.html'
|
|
9
|
+
OPT_DEFAULT_TITLE = 'Cron Tasks'
|
|
10
|
+
OPT_DEFAULT_WIDTH = 100
|
|
11
|
+
|
|
12
|
+
attr_accessor :output_file_path, :duration, :from_date, :from_time, :title, :width
|
|
13
|
+
|
|
14
|
+
def to_from_time
|
|
15
|
+
parsed = Time.strptime("#{@from_date} #{@from_time}", "#{OPT_DATE_FORMAT} #{OPT_TIME_FORMAT}")
|
|
16
|
+
Time.utc(parsed.year, parsed.month, parsed.day, parsed.hour, parsed.min, 0)
|
|
17
|
+
rescue ArgumentError => e
|
|
18
|
+
raise ArgumentError, "invalid date/time format: '#{@from_date} #{@from_time}', #{e.message}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_duration_minutes
|
|
22
|
+
raise ArgumentError, "invalid duration format: '#{@duration}'" if @duration.length < 2
|
|
23
|
+
|
|
24
|
+
duration_str = @duration[0..-2]
|
|
25
|
+
unit = @duration[-1]
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
duration_i = Integer(duration_str)
|
|
29
|
+
rescue ArgumentError => e
|
|
30
|
+
raise ArgumentError, "invalid duration format: '#{@duration}', #{e.message}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
case unit.downcase
|
|
34
|
+
when 'd'
|
|
35
|
+
Float(duration_i * 24 * 60)
|
|
36
|
+
when 'h'
|
|
37
|
+
Float(duration_i * 60)
|
|
38
|
+
when 'm'
|
|
39
|
+
Float(duration_i)
|
|
40
|
+
else
|
|
41
|
+
raise ArgumentError, "invalid duration format: '#{@duration}', '#{unit}' is not in d/h/m"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.new_cronv_option(now)
|
|
46
|
+
utc_now = now.utc
|
|
47
|
+
new(utc_now.strftime(OPT_DATE_FORMAT), utc_now.strftime(OPT_TIME_FORMAT))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def initialize(from_date, from_time)
|
|
53
|
+
@output_file_path = OPT_DEFAULT_OUTPUT_PATH
|
|
54
|
+
@duration = OPT_DEFAULT_DURATION
|
|
55
|
+
@from_date = from_date
|
|
56
|
+
@from_time = from_time
|
|
57
|
+
@title = OPT_DEFAULT_TITLE
|
|
58
|
+
@width = OPT_DEFAULT_WIDTH
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fugit'
|
|
4
|
+
|
|
5
|
+
module CronvRb
|
|
6
|
+
class Record
|
|
7
|
+
class << self
|
|
8
|
+
def new_record(line:, start_time:, duration_minutes:, context: {})
|
|
9
|
+
crontab, extra = Crontab.parse(context, line)
|
|
10
|
+
|
|
11
|
+
return nil, extra if crontab.nil?
|
|
12
|
+
|
|
13
|
+
fugit_cron = Fugit.parse_cron(crontab.schedule.to_crontab)
|
|
14
|
+
|
|
15
|
+
new(crontab:, fugit_cron:, start_time:, duration_minutes:)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :crontab, :fugit_cron, :start_time, :duration_minutes
|
|
20
|
+
|
|
21
|
+
def initialize(crontab:, fugit_cron:, start_time:, duration_minutes:)
|
|
22
|
+
@crontab = crontab
|
|
23
|
+
@fugit_cron = fugit_cron
|
|
24
|
+
# start_time class Time
|
|
25
|
+
@start_time = start_time
|
|
26
|
+
# duration_minutes class Integer
|
|
27
|
+
@duration_minutes = duration_minutes
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Exec = Struct.new(:start, :end)
|
|
31
|
+
|
|
32
|
+
def iter
|
|
33
|
+
array = []
|
|
34
|
+
|
|
35
|
+
end_time = @start_time + (@duration_minutes * 60)
|
|
36
|
+
return array if @fugit_cron.nil?
|
|
37
|
+
|
|
38
|
+
# next_time is an EtOrbi::EoTime; use .to_t (converts to plain Ruby Time)
|
|
39
|
+
# for comparison to avoid timezone label mismatches ("Z" vs "UTC") that
|
|
40
|
+
# cause string comparison to fail for executions at the exact end boundary.
|
|
41
|
+
@next_time = @fugit_cron.next_time(@start_time.to_s)
|
|
42
|
+
|
|
43
|
+
while @next_time.to_t <= end_time
|
|
44
|
+
array.push({ start: @next_time.to_s, end: (@next_time + 60).to_s })
|
|
45
|
+
@next_time = @fugit_cron.next_time(@next_time.to_s)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
array
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|