dirigent-examples 0.15.0__py3-none-any.whl
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.
- dirigent_examples/__init__.py +22 -0
- dirigent_examples/py.typed +0 -0
- dirigent_examples/shelves/README.md +299 -0
- dirigent_examples/shelves/composition/README.md +18 -0
- dirigent_examples/shelves/composition/chained-instances.yaml +97 -0
- dirigent_examples/shelves/composition/composition-child.yaml +64 -0
- dirigent_examples/shelves/composition/composition-parent.yaml +89 -0
- dirigent_examples/shelves/connections.yaml +52 -0
- dirigent_examples/shelves/demo/README.md +19 -0
- dirigent_examples/shelves/demo/markdown-showcase.yaml +117 -0
- dirigent_examples/shelves/demo/params-showcase.yaml +92 -0
- dirigent_examples/shelves/demo/requires.yaml +65 -0
- dirigent_examples/shelves/demo/weekly-import-malawi.yaml +54 -0
- dirigent_examples/shelves/demo/weekly-import-nepal.yaml +75 -0
- dirigent_examples/shelves/docker/README.md +29 -0
- dirigent_examples/shelves/docker/docker-build-push.yaml +110 -0
- dirigent_examples/shelves/docker/docker-build-run.yaml +106 -0
- dirigent_examples/shelves/docker/docker-compose-database.yaml +124 -0
- dirigent_examples/shelves/docker/docker-compose-failing-up.yaml +83 -0
- dirigent_examples/shelves/docker/docker-compose-file.yaml +117 -0
- dirigent_examples/shelves/docker/docker-compose-profiles-env.yaml +133 -0
- dirigent_examples/shelves/docker/docker-compose-stack.yaml +70 -0
- dirigent_examples/shelves/docker/docker-hello.yaml +53 -0
- dirigent_examples/shelves/docker/docker-remote-daemon.yaml +92 -0
- dirigent_examples/shelves/docker/docker-run-failing-teardown.yaml +94 -0
- dirigent_examples/shelves/docker/docker-ticker.yaml +49 -0
- dirigent_examples/shelves/execute/README.md +16 -0
- dirigent_examples/shelves/execute/long-log.yaml +89 -0
- dirigent_examples/shelves/failure/README.md +20 -0
- dirigent_examples/shelves/failure/error-handler.yaml +89 -0
- dirigent_examples/shelves/failure/optional-step.yaml +82 -0
- dirigent_examples/shelves/failure/retries.yaml +75 -0
- dirigent_examples/shelves/failure/retry-budget.yaml +82 -0
- dirigent_examples/shelves/failure/step-timeout.yaml +96 -0
- dirigent_examples/shelves/git/README.md +32 -0
- dirigent_examples/shelves/git/git-checkout-build.yaml +125 -0
- dirigent_examples/shelves/git/git-checkout-compose.yaml +138 -0
- dirigent_examples/shelves/git/git-checkout-public.yaml +84 -0
- dirigent_examples/shelves/graph/README.md +22 -0
- dirigent_examples/shelves/graph/deep-chain.yaml +119 -0
- dirigent_examples/shelves/graph/fan-in.yaml +80 -0
- dirigent_examples/shelves/graph/fan-out.yaml +66 -0
- dirigent_examples/shelves/graph/linear.yaml +66 -0
- dirigent_examples/shelves/graph/parallel-branches.yaml +57 -0
- dirigent_examples/shelves/graph/parallel-sleep.yaml +55 -0
- dirigent_examples/shelves/graph/skip-diamond.yaml +105 -0
- dirigent_examples/shelves/graph/wide-fan.yaml +147 -0
- dirigent_examples/shelves/hello-world.yaml +30 -0
- dirigent_examples/shelves/open-data/README.md +67 -0
- dirigent_examples/shelves/open-data/feeds-composition.yaml +120 -0
- dirigent_examples/shelves/open-data/gdacs-disaster-updates.yaml +230 -0
- dirigent_examples/shelves/open-data/github-releases-relay.yaml +225 -0
- dirigent_examples/shelves/open-data/hdx-dataset-watch.yaml +211 -0
- dirigent_examples/shelves/open-data/kobo-submissions-to-csv.yaml +149 -0
- dirigent_examples/shelves/open-data/nominatim-geocode-facilities.yaml +169 -0
- dirigent_examples/shelves/open-data/odk-central-submissions.yaml +146 -0
- dirigent_examples/shelves/open-data/open-meteo-weekly-report.yaml +142 -0
- dirigent_examples/shelves/open-data/overpass-health-facilities.yaml +154 -0
- dirigent_examples/shelves/open-data/usgs-earthquakes-alert.yaml +208 -0
- dirigent_examples/shelves/open-data/who-gho-indicators-to-parquet.yaml +163 -0
- dirigent_examples/shelves/open-data/wikidata-country-reference.yaml +146 -0
- dirigent_examples/shelves/open-data/world-bank-population-trend.yaml +166 -0
- dirigent_examples/shelves/patterns/README.md +144 -0
- dirigent_examples/shelves/patterns/concurrency-queue.yaml +89 -0
- dirigent_examples/shelves/patterns/concurrency-replace.yaml +92 -0
- dirigent_examples/shelves/patterns/concurrency-skip.yaml +97 -0
- dirigent_examples/shelves/patterns/connections-referenced-vs-carried.yaml +140 -0
- dirigent_examples/shelves/patterns/deadline-on-a-sensor.yaml +106 -0
- dirigent_examples/shelves/patterns/fan-out-continue.yaml +88 -0
- dirigent_examples/shelves/patterns/fan-out-fail-fast.yaml +80 -0
- dirigent_examples/shelves/patterns/fan-out-from-params.yaml +84 -0
- dirigent_examples/shelves/patterns/fan-out-item-wise.yaml +111 -0
- dirigent_examples/shelves/patterns/fan-out-literal-list.yaml +81 -0
- dirigent_examples/shelves/patterns/fan-out-nested-objects.yaml +107 -0
- dirigent_examples/shelves/patterns/fan-out-then-join.yaml +86 -0
- dirigent_examples/shelves/patterns/log-levels.yaml +119 -0
- dirigent_examples/shelves/patterns/outputs-inline-vs-storage.yaml +140 -0
- dirigent_examples/shelves/patterns/params-every-type.yaml +259 -0
- dirigent_examples/shelves/patterns/params-validation-refuses.yaml +131 -0
- dirigent_examples/shelves/patterns/pipeline-run-child.yaml +96 -0
- dirigent_examples/shelves/patterns/pipeline-run-fire-and-forget.yaml +99 -0
- dirigent_examples/shelves/patterns/pipeline-run-strict.yaml +103 -0
- dirigent_examples/shelves/patterns/pipeline-run-wait.yaml +107 -0
- dirigent_examples/shelves/patterns/pipeline-run-with-params.yaml +124 -0
- dirigent_examples/shelves/patterns/poll-cadence.yaml +102 -0
- dirigent_examples/shelves/patterns/priority-layered.yaml +120 -0
- dirigent_examples/shelves/patterns/references-cheat-sheet.yaml +186 -0
- dirigent_examples/shelves/patterns/retry-budget-exhausted.yaml +92 -0
- dirigent_examples/shelves/patterns/retry-exponential-backoff.yaml +88 -0
- dirigent_examples/shelves/patterns/retry-only-transient.yaml +108 -0
- dirigent_examples/shelves/patterns/retry-with-jitter.yaml +102 -0
- dirigent_examples/shelves/patterns/rule-all-done.yaml +83 -0
- dirigent_examples/shelves/patterns/rule-all-success.yaml +79 -0
- dirigent_examples/shelves/patterns/rule-always.yaml +92 -0
- dirigent_examples/shelves/patterns/rule-one-failed.yaml +86 -0
- dirigent_examples/shelves/patterns/schedule-at-once.yaml +110 -0
- dirigent_examples/shelves/patterns/schedule-cron-timezone.yaml +121 -0
- dirigent_examples/shelves/patterns/schedule-interval.yaml +109 -0
- dirigent_examples/shelves/patterns/schedule-window-half-open.yaml +105 -0
- dirigent_examples/shelves/patterns/sensor-http-ready.yaml +121 -0
- dirigent_examples/shelves/patterns/sensor-storage-exists.yaml +134 -0
- dirigent_examples/shelves/patterns/step-names-and-keys.yaml +99 -0
- dirigent_examples/shelves/patterns/timeout-fails-the-step.yaml +94 -0
- dirigent_examples/shelves/patterns/timeout-skips-the-step.yaml +102 -0
- dirigent_examples/shelves/patterns/webhook-mapping-nested-payload.yaml +125 -0
- dirigent_examples/shelves/patterns/webhook-signed.yaml +144 -0
- dirigent_examples/shelves/preview/s3-parquet-to-ingestion.yaml +92 -0
- dirigent_examples/shelves/python/README.md +31 -0
- dirigent_examples/shelves/python/apply_and_run.py +52 -0
- dirigent_examples/shelves/python/ci_gate.py +76 -0
- dirigent_examples/shelves/python/connections.py +61 -0
- dirigent_examples/shelves/python/error_handling.py +84 -0
- dirigent_examples/shelves/python/follow_logs.py +39 -0
- dirigent_examples/shelves/python/list_and_filter.py +52 -0
- dirigent_examples/shelves/queues/README.md +59 -0
- dirigent_examples/shelves/queues/kafka-consume-then-transform.yaml +105 -0
- dirigent_examples/shelves/queues/kafka-produce-then-consume.yaml +124 -0
- dirigent_examples/shelves/queues/rabbitmq-consume-ack-on-success.yaml +102 -0
- dirigent_examples/shelves/queues/report-to-kafka.yaml +105 -0
- dirigent_examples/shelves/queues/report-to-rabbitmq.yaml +105 -0
- dirigent_examples/shelves/recipes/README.md +130 -0
- dirigent_examples/shelves/recipes/csv-header-rules.yaml +147 -0
- dirigent_examples/shelves/recipes/csv-to-ndjson.yaml +107 -0
- dirigent_examples/shelves/recipes/etl-csv-clean-validate-parquet.yaml +207 -0
- dirigent_examples/shelves/recipes/filter-by-predicate.yaml +116 -0
- dirigent_examples/shelves/recipes/filter-then-map-then-reduce.yaml +109 -0
- dirigent_examples/shelves/recipes/http-fetch-validate-post.yaml +142 -0
- dirigent_examples/shelves/recipes/http-follow-redirects.yaml +100 -0
- dirigent_examples/shelves/recipes/http-get-with-query.yaml +96 -0
- dirigent_examples/shelves/recipes/http-headers-and-auth-connection.yaml +110 -0
- dirigent_examples/shelves/recipes/http-post-file-from-storage.yaml +117 -0
- dirigent_examples/shelves/recipes/http-post-json-echo.yaml +105 -0
- dirigent_examples/shelves/recipes/http-post-report.yaml +183 -0
- dirigent_examples/shelves/recipes/http-save-body-to-storage.yaml +106 -0
- dirigent_examples/shelves/recipes/http-success-status-list.yaml +80 -0
- dirigent_examples/shelves/recipes/http-timeout-override.yaml +104 -0
- dirigent_examples/shelves/recipes/jq-dedupe-by-key.yaml +78 -0
- dirigent_examples/shelves/recipes/jq-defaults-and-nulls.yaml +91 -0
- dirigent_examples/shelves/recipes/jq-group-by-and-sum.yaml +74 -0
- dirigent_examples/shelves/recipes/jq-join-two-lists.yaml +77 -0
- dirigent_examples/shelves/recipes/jq-long-to-wide.yaml +76 -0
- dirigent_examples/shelves/recipes/jq-nested-to-flat.yaml +89 -0
- dirigent_examples/shelves/recipes/jq-pivot-wide-to-long.yaml +65 -0
- dirigent_examples/shelves/recipes/jq-running-totals.yaml +82 -0
- dirigent_examples/shelves/recipes/jq-string-cleaning.yaml +88 -0
- dirigent_examples/shelves/recipes/jq-top-n.yaml +85 -0
- dirigent_examples/shelves/recipes/jq-validate-in-jq-vs-schema.yaml +124 -0
- dirigent_examples/shelves/recipes/jq-window-dates.yaml +82 -0
- dirigent_examples/shelves/recipes/json-to-csv-flattening.yaml +141 -0
- dirigent_examples/shelves/recipes/large-output-to-storage.yaml +134 -0
- dirigent_examples/shelves/recipes/map-enrich-with-lookup.yaml +96 -0
- dirigent_examples/shelves/recipes/ndjson-to-parquet.yaml +130 -0
- dirigent_examples/shelves/recipes/pagination-by-fan-out.yaml +115 -0
- dirigent_examples/shelves/recipes/parquet-round-trip-types.yaml +163 -0
- dirigent_examples/shelves/recipes/reconcile-two-sources.yaml +159 -0
- dirigent_examples/shelves/recipes/report-built-in.yaml +72 -0
- dirigent_examples/shelves/recipes/report-daily-digest.yaml +186 -0
- dirigent_examples/shelves/recipes/report-to-file.yaml +131 -0
- dirigent_examples/shelves/recipes/report-to-webhook.yaml +136 -0
- dirigent_examples/shelves/recipes/schema-carried.yaml +112 -0
- dirigent_examples/shelves/recipes/schema-formats.yaml +107 -0
- dirigent_examples/shelves/recipes/schema-referenced.yaml +86 -0
- dirigent_examples/shelves/recipes/schema-refuses-then-rule.yaml +127 -0
- dirigent_examples/shelves/recipes/storage-copy-dated-archive.yaml +114 -0
- dirigent_examples/shelves/recipes/storage-exists-gate.yaml +127 -0
- dirigent_examples/shelves/recipes/storage-manifest-of-a-fan-out.yaml +104 -0
- dirigent_examples/shelves/recipes/storage-write-then-read.yaml +119 -0
- dirigent_examples/shelves/recipes/webhook-post-hmac.yaml +132 -0
- dirigent_examples/shelves/recipes/webhook-post-summary.yaml +142 -0
- dirigent_examples/shelves/s3/README.md +34 -0
- dirigent_examples/shelves/s3/report-to-s3.yaml +93 -0
- dirigent_examples/shelves/s3/s3-copy-and-verify.yaml +105 -0
- dirigent_examples/shelves/s3/s3-csv-report.yaml +87 -0
- dirigent_examples/shelves/s3/s3-parquet-report.yaml +77 -0
- dirigent_examples/shelves/s3/s3-round-trip.yaml +125 -0
- dirigent_examples/shelves/schemas/README.md +36 -0
- dirigent_examples/shelves/schemas/echo-reading.json +18 -0
- dirigent_examples/shelves/schemas/ou-record.json +13 -0
- dirigent_examples/shelves/schemas/station-reading.json +13 -0
- dirigent_examples/shelves/sensors/README.md +16 -0
- dirigent_examples/shelves/sensors/sensor-gate.yaml +65 -0
- dirigent_examples/shelves/sensors/time-window.yaml +61 -0
- dirigent_examples/shelves/sql/README.md +52 -0
- dirigent_examples/shelves/sql/duckdb-parquet-to-report.yaml +146 -0
- dirigent_examples/shelves/sql/sql-postgres-readonly.yaml +111 -0
- dirigent_examples/shelves/sql/sql-query-to-storage.yaml +85 -0
- dirigent_examples/shelves/sql/sql-sqlite-roundtrip.yaml +114 -0
- dirigent_examples/shelves/sql/warehouse.sql +42 -0
- dirigent_examples/shelves/transform/README.md +36 -0
- dirigent_examples/shelves/transform/csv-report.yaml +55 -0
- dirigent_examples/shelves/transform/jq-filter-and-map.yaml +70 -0
- dirigent_examples/shelves/transform/jq-group-and-aggregate.yaml +70 -0
- dirigent_examples/shelves/transform/jq-join-two-sources.yaml +98 -0
- dirigent_examples/shelves/transform/jq-reshape.yaml +91 -0
- dirigent_examples/shelves/transform/jq-stream-through-storage.yaml +112 -0
- dirigent_examples/shelves/transform/ndjson-round-trip.yaml +56 -0
- dirigent_examples/shelves/transform/parquet-round-trip.yaml +68 -0
- dirigent_examples/shelves/transform/std-convert-fan-out.yaml +142 -0
- dirigent_examples/shelves/transform/xml-feed-to-ndjson.yaml +116 -0
- dirigent_examples/shelves/transform/yaml-config-to-json.yaml +104 -0
- dirigent_examples/shelves/triggers/README.md +45 -0
- dirigent_examples/shelves/triggers/at-one-time.yaml +78 -0
- dirigent_examples/shelves/triggers/cron-nightly.yaml +79 -0
- dirigent_examples/shelves/triggers/cron-windowed.yaml +86 -0
- dirigent_examples/shelves/triggers/document-nightly.yaml +80 -0
- dirigent_examples/shelves/triggers/interval-rolling.yaml +88 -0
- dirigent_examples/shelves/triggers/managed-and-manual.yaml +109 -0
- dirigent_examples/shelves/triggers/webhook-trigger.yaml +75 -0
- dirigent_examples/shelves/validate/README.md +31 -0
- dirigent_examples/shelves/validate/expects-a-shape.yaml +56 -0
- dirigent_examples/shelves/validate/the-shape-is-wrong.yaml +46 -0
- dirigent_examples-0.15.0.dist-info/METADATA +21 -0
- dirigent_examples-0.15.0.dist-info/RECORD +216 -0
- dirigent_examples-0.15.0.dist-info/WHEEL +4 -0
- dirigent_examples-0.15.0.dist-info/entry_points.txt +3 -0
- dirigent_examples-0.15.0.dist-info/licenses/LICENSE +18 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Two of the feeds on this shelf, started by a third: refresh the reference, then read the data.
|
|
2
|
+
#
|
|
3
|
+
# `pipeline.run` starts another pipeline on the same instance and, by default, waits for it. So
|
|
4
|
+
# this document is an orchestration layer over documents that each stand alone: the Wikidata
|
|
5
|
+
# reference table and the WHO GHO extract know nothing about each other or about this file, and
|
|
6
|
+
# each is still runnable, testable and schedulable on its own.
|
|
7
|
+
#
|
|
8
|
+
# WHAT CROSSES THE BOUNDARY, AND WHAT DOES NOT. Parameters go down: the parent hands each child
|
|
9
|
+
# exactly the ones that child's own schema declares, validated against that schema when the step
|
|
10
|
+
# executes rather than when this document is applied. What comes back up is a status and a run
|
|
11
|
+
# id -- not the child's data. So the country list below is written here rather than read out of
|
|
12
|
+
# the reference the first child just built: a child's rows reach a parent only through storage,
|
|
13
|
+
# at a path both sides agree on, and under `--local` each run's scratch is its own. On an
|
|
14
|
+
# instance, giving both children a shared prefix parameter is what turns "refresh the reference"
|
|
15
|
+
# into something the next child can actually read.
|
|
16
|
+
#
|
|
17
|
+
# `requires.pipelines` names both children, so an instance that does not hold them refuses this
|
|
18
|
+
# document at apply time with the list of what to apply first, instead of failing at the step at
|
|
19
|
+
# six in the morning.
|
|
20
|
+
#
|
|
21
|
+
# What happens, hop by hop:
|
|
22
|
+
#
|
|
23
|
+
# reference the Wikidata country table, rebuilt. It takes no parameters: what it produces is
|
|
24
|
+
# the same for everybody, which is what makes it a reference.
|
|
25
|
+
# indicators the GHO extract for one indicator over the country list, started after the
|
|
26
|
+
# reference finished. It fans out internally with items: continue, so a country the
|
|
27
|
+
# endpoint refuses leaves that child completed_with_errors -- and `strict` is left
|
|
28
|
+
# at its default here, so that outcome is reported rather than treated as failure.
|
|
29
|
+
# Setting strict: true is how a parent says a partial child is not good enough.
|
|
30
|
+
# briefing the receipt: which children ran, under which run ids, and how each ended. A run
|
|
31
|
+
# id is what somebody opens to see what actually happened inside a child.
|
|
32
|
+
#
|
|
33
|
+
# To make it yours: add a child. A briefing is a list of pipeline.run steps and a transform that
|
|
34
|
+
# reads their statuses, and adding a feed is one step plus one line in requires.
|
|
35
|
+
#
|
|
36
|
+
# dg run --local examples/open-data/feeds-composition.yaml \
|
|
37
|
+
# --also-apply examples/open-data/wikidata-country-reference.yaml \
|
|
38
|
+
# --also-apply examples/open-data/who-gho-indicators-to-parquet.yaml
|
|
39
|
+
#
|
|
40
|
+
# Against an instance the children are applied first, and then the parent is just a pipeline:
|
|
41
|
+
#
|
|
42
|
+
# dg apply examples/open-data/wikidata-country-reference.yaml
|
|
43
|
+
# dg apply examples/open-data/who-gho-indicators-to-parquet.yaml
|
|
44
|
+
# dg apply examples/open-data/feeds-composition.yaml
|
|
45
|
+
# dg run feeds-composition --watch
|
|
46
|
+
|
|
47
|
+
format: dirigent/v1
|
|
48
|
+
kind: pipeline
|
|
49
|
+
code: feeds-composition
|
|
50
|
+
name: Open data briefing
|
|
51
|
+
description: |
|
|
52
|
+
One document that runs two others: the **Wikidata** country reference and the **WHO GHO**
|
|
53
|
+
indicator extract, then reports how each child ended.
|
|
54
|
+
|
|
55
|
+
Parameters go down, statuses come back up, and data crosses between pipelines through
|
|
56
|
+
storage rather than through a step output.
|
|
57
|
+
|
|
58
|
+
tags: [open-data, pipeline, transform, briefing, composition]
|
|
59
|
+
|
|
60
|
+
requires:
|
|
61
|
+
blocks:
|
|
62
|
+
- pipeline.run
|
|
63
|
+
- transform.jq
|
|
64
|
+
pipelines:
|
|
65
|
+
- wikidata-country-reference
|
|
66
|
+
- who-gho-indicators-to-parquet
|
|
67
|
+
|
|
68
|
+
params:
|
|
69
|
+
type: object
|
|
70
|
+
additionalProperties: false
|
|
71
|
+
properties:
|
|
72
|
+
indicator:
|
|
73
|
+
type: string
|
|
74
|
+
default: WHOSIS_000001
|
|
75
|
+
description: Handed to the GHO extract as its own indicator parameter.
|
|
76
|
+
countries:
|
|
77
|
+
type: array
|
|
78
|
+
default: [MWI, NPL]
|
|
79
|
+
items:
|
|
80
|
+
type: string
|
|
81
|
+
description: The countries the extract fans out over.
|
|
82
|
+
|
|
83
|
+
steps:
|
|
84
|
+
reference:
|
|
85
|
+
block: pipeline.run
|
|
86
|
+
config:
|
|
87
|
+
pipeline: wikidata-country-reference
|
|
88
|
+
# No params: the child's defaults are the whole answer, and a parent that repeats them
|
|
89
|
+
# here would be a second place to change them.
|
|
90
|
+
|
|
91
|
+
indicators:
|
|
92
|
+
block: pipeline.run
|
|
93
|
+
depends_on: [reference]
|
|
94
|
+
config:
|
|
95
|
+
pipeline: who-gho-indicators-to-parquet
|
|
96
|
+
# Only what the child's schema declares. A parameter it does not have is refused when
|
|
97
|
+
# this step executes, which is the same check a person typing -p would get.
|
|
98
|
+
params:
|
|
99
|
+
indicator: ${params.indicator}
|
|
100
|
+
countries: ${params.countries}
|
|
101
|
+
|
|
102
|
+
briefing:
|
|
103
|
+
block: transform.jq
|
|
104
|
+
depends_on: [indicators]
|
|
105
|
+
config:
|
|
106
|
+
input:
|
|
107
|
+
indicator: ${params.indicator}
|
|
108
|
+
reference: ${steps.reference.output}
|
|
109
|
+
indicators: ${steps.indicators.output}
|
|
110
|
+
# A child hands back three things and no data: which pipeline ran, which run it was, and
|
|
111
|
+
# how it ended.
|
|
112
|
+
program: |
|
|
113
|
+
{
|
|
114
|
+
indicator,
|
|
115
|
+
children: [
|
|
116
|
+
{feed: .reference.pipeline, run: .reference.run_id, status: .reference.status},
|
|
117
|
+
{feed: .indicators.pipeline, run: .indicators.run_id, status: .indicators.status}
|
|
118
|
+
],
|
|
119
|
+
all_green: ([.reference.status, .indicators.status] | all(. == "succeeded"))
|
|
120
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Yesterday's disaster alerts, with the ones you already saw taken out.
|
|
2
|
+
#
|
|
3
|
+
# GDACS -- the Global Disaster Alert and Coordination System, run by the JRC and OCHA -- is
|
|
4
|
+
# keyless and anonymous: no account, no token, no registered application. One GET answers
|
|
5
|
+
# GeoJSON, {"features": [{"properties": {...}}]}, and everything an event has is under
|
|
6
|
+
# `properties`.
|
|
7
|
+
#
|
|
8
|
+
# The window does not make the query non-overlapping. GDACS filters on the event's own active
|
|
9
|
+
# range, so a cyclone that opened last week and has not closed comes back in today's answer and
|
|
10
|
+
# in tomorrow's, and an event that is revised keeps its `eventid` while `datemodified` moves.
|
|
11
|
+
# Both are the same problem: the query cannot tell you what is new. The fix is a list of what
|
|
12
|
+
# yesterday's run saw, and the honest consequence is visible in the graph: yesterday's file is
|
|
13
|
+
# read behind a sensor, so on the very first day there is nothing to compare against, `seen`
|
|
14
|
+
# skips, and everything behind it skips too. Day one posts nothing and ends succeeded; day two
|
|
15
|
+
# posts what day one did not have. There is no state block, so a marker file is the state, and a
|
|
16
|
+
# skipped read is how a pipeline says "there was no marker".
|
|
17
|
+
#
|
|
18
|
+
# What happens, hop by hop:
|
|
19
|
+
#
|
|
20
|
+
# dates the window turned into the two dates -- the ones the query filters between, and
|
|
21
|
+
# the ones the files below are named by. GDACS wants a plain day here, not the
|
|
22
|
+
# instant a window edge carries. It settles the prefix the lists are kept under
|
|
23
|
+
# as well, since the default one is only known once the run has started.
|
|
24
|
+
# fetch one GET. The two list filters are semicolon-joined in a single parameter, which is
|
|
25
|
+
# this API's own convention rather than a general one.
|
|
26
|
+
# today the events flattened to rows, carried as a value because the comparison needs them
|
|
27
|
+
# as data.
|
|
28
|
+
# record storage.write, this day's list, put down before anything is compared so tomorrow's
|
|
29
|
+
# run has something to read even if the post below fails.
|
|
30
|
+
# seen yesterday's list, if there is one. Five seconds, then skip: an absent file is the
|
|
31
|
+
# expected answer on day one and after any gap in the schedule.
|
|
32
|
+
# recalled storage.read, yesterday's file brought back into the run as a value. A value
|
|
33
|
+
# goes out to storage only through a write and comes in only through a read, so
|
|
34
|
+
# the marker costs a step at each end.
|
|
35
|
+
# previous yesterday's ids, taken out of what that read produced.
|
|
36
|
+
# fresh the difference. The comparison is over ids alone, because a revised event keeps
|
|
37
|
+
# its id and moves its date.
|
|
38
|
+
# notify what is actually new, posted. Postman Echo by default so the shape of the post is
|
|
39
|
+
# visible without a receiver; point it at yours.
|
|
40
|
+
#
|
|
41
|
+
# To make it yours: narrow `eventlist` and `alertlevel` to what you act on, and give `store` a
|
|
42
|
+
# prefix that outlives one run. The default is `${run.scratch}`, which is deleted with the run,
|
|
43
|
+
# so under it every day is day one.
|
|
44
|
+
#
|
|
45
|
+
# Rehearsing both days from the CLI takes one directory holding the instance, and a `store`
|
|
46
|
+
# under that directory's artifact root so the second run finds what the first wrote:
|
|
47
|
+
#
|
|
48
|
+
# dg run --local --root /tmp/dirigent-gdacs examples/open-data/gdacs-disaster-updates.yaml \
|
|
49
|
+
# -p store=file:///tmp/dirigent-gdacs/artifacts/gdacs --window 2026-09-05..2026-09-06
|
|
50
|
+
# dg run --local --root /tmp/dirigent-gdacs examples/open-data/gdacs-disaster-updates.yaml \
|
|
51
|
+
# -p store=file:///tmp/dirigent-gdacs/artifacts/gdacs --window 2026-09-06..2026-09-07
|
|
52
|
+
#
|
|
53
|
+
# Day one skips at `seen` and posts nothing; day two posts the difference.
|
|
54
|
+
|
|
55
|
+
format: dirigent/v1
|
|
56
|
+
kind: pipeline
|
|
57
|
+
code: gdacs-disaster-updates
|
|
58
|
+
name: GDACS disaster updates
|
|
59
|
+
description: |
|
|
60
|
+
Disaster events active inside the run's window, read from the **GDACS** event list,
|
|
61
|
+
deduplicated against the previous day's list and posted onward.
|
|
62
|
+
|
|
63
|
+
Keyless and anonymous: nothing to register, nothing to hold.
|
|
64
|
+
|
|
65
|
+
tags: [open-data, http, sensor, storage, transform, webhook, schedule]
|
|
66
|
+
|
|
67
|
+
requires:
|
|
68
|
+
blocks:
|
|
69
|
+
- transform.jq
|
|
70
|
+
- http.request
|
|
71
|
+
- storage.write
|
|
72
|
+
- storage.exists
|
|
73
|
+
- storage.read
|
|
74
|
+
- webhook.post
|
|
75
|
+
|
|
76
|
+
params:
|
|
77
|
+
type: object
|
|
78
|
+
additionalProperties: false
|
|
79
|
+
properties:
|
|
80
|
+
eventlist:
|
|
81
|
+
type: string
|
|
82
|
+
default: EQ;TC;FL
|
|
83
|
+
description: >-
|
|
84
|
+
Event types, semicolon-separated as GDACS reads them: EQ earthquake, TC tropical
|
|
85
|
+
cyclone, FL flood, VO volcano, DR drought, WF wildfire.
|
|
86
|
+
alertlevel:
|
|
87
|
+
type: string
|
|
88
|
+
default: Green;Orange;Red
|
|
89
|
+
description: Alert levels, semicolon-separated. Orange;Red is the pair worth waking for.
|
|
90
|
+
store:
|
|
91
|
+
type: string
|
|
92
|
+
default: ""
|
|
93
|
+
description: >-
|
|
94
|
+
A prefix the daily lists are written under, such as
|
|
95
|
+
file:///var/lib/dirigent/gdacs. Empty keeps them in the run's own scratch, where
|
|
96
|
+
tomorrow's run cannot read them.
|
|
97
|
+
webhook_url:
|
|
98
|
+
type: string
|
|
99
|
+
default: https://postman-echo.com/post
|
|
100
|
+
description: Where the digest goes.
|
|
101
|
+
|
|
102
|
+
steps:
|
|
103
|
+
dates:
|
|
104
|
+
block: transform.jq
|
|
105
|
+
config:
|
|
106
|
+
input:
|
|
107
|
+
start: ${run.window.start}
|
|
108
|
+
end: ${run.window.end}
|
|
109
|
+
store: ${params.store}
|
|
110
|
+
scratch: ${run.scratch}
|
|
111
|
+
# The two days the query filters between, and the two file names. Yesterday is the day
|
|
112
|
+
# before the window opened, which is the file the previous firing wrote. The prefix is
|
|
113
|
+
# settled here too: a reference resolves once, so a default that names another one has
|
|
114
|
+
# to be worked out in a step rather than written in the parameter.
|
|
115
|
+
program: |
|
|
116
|
+
def as_date: .[0:19] + "Z" | fromdateiso8601;
|
|
117
|
+
{
|
|
118
|
+
day: .start[0:10],
|
|
119
|
+
from_date: .start[0:10],
|
|
120
|
+
to_date: .end[0:10],
|
|
121
|
+
previous_day: (.start | as_date | . - 86400 | strftime("%Y-%m-%d")),
|
|
122
|
+
store: (if .store == "" then .scratch + "/gdacs" else .store end)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fetch:
|
|
126
|
+
block: http.request
|
|
127
|
+
depends_on: [dates]
|
|
128
|
+
config:
|
|
129
|
+
url: https://www.gdacs.org/gdacsapi/api/events/geteventlist/SEARCH
|
|
130
|
+
query:
|
|
131
|
+
# Both filters are lists written into one parameter, joined with semicolons. This is
|
|
132
|
+
# GDACS's own convention, not a general one.
|
|
133
|
+
eventlist: ${params.eventlist}
|
|
134
|
+
alertlevel: ${params.alertlevel}
|
|
135
|
+
# A plain day at each edge. The filter is an overlap against the event's own from/to
|
|
136
|
+
# range, so an event still running when the window opened is included.
|
|
137
|
+
fromDate: ${steps.dates.output.value.from_date}
|
|
138
|
+
toDate: ${steps.dates.output.value.to_date}
|
|
139
|
+
# The API's own page ceiling. A single day worldwide is far short of it; a longer
|
|
140
|
+
# window is not, and would need pageNumber walked.
|
|
141
|
+
pageSize: 100
|
|
142
|
+
|
|
143
|
+
today:
|
|
144
|
+
block: transform.jq
|
|
145
|
+
depends_on: [fetch]
|
|
146
|
+
config:
|
|
147
|
+
input: ${steps.fetch.output.body}
|
|
148
|
+
# GeoJSON, so the event is under properties. The id is the type and the number together:
|
|
149
|
+
# GDACS's own detail URL takes both, and the number alone is a sequence per event type.
|
|
150
|
+
program: |
|
|
151
|
+
[.features[]
|
|
152
|
+
| .properties as $p
|
|
153
|
+
| {
|
|
154
|
+
id: "\($p.eventtype)-\($p.eventid)",
|
|
155
|
+
title: $p.name,
|
|
156
|
+
url: $p.url.report,
|
|
157
|
+
event_type: $p.eventtype,
|
|
158
|
+
alert_level: $p.alertlevel,
|
|
159
|
+
country: $p.country,
|
|
160
|
+
from_date: $p.fromdate,
|
|
161
|
+
modified_at: $p.datemodified
|
|
162
|
+
}]
|
|
163
|
+
|
|
164
|
+
record:
|
|
165
|
+
block: storage.write
|
|
166
|
+
depends_on: [dates, today]
|
|
167
|
+
config:
|
|
168
|
+
# Written before anything is compared, so tomorrow's run has a list to read even if the
|
|
169
|
+
# post below fails. The day names the file, which is what makes "yesterday" a path.
|
|
170
|
+
target: ${steps.dates.output.value.store}/${steps.dates.output.value.day}.json
|
|
171
|
+
value: ${steps.today.output.value}
|
|
172
|
+
|
|
173
|
+
seen:
|
|
174
|
+
block: storage.exists
|
|
175
|
+
depends_on: [dates, record]
|
|
176
|
+
poll: 1s
|
|
177
|
+
deadline: 5s
|
|
178
|
+
# No file for yesterday means no previous run, or a gap in the schedule. Both are quiet
|
|
179
|
+
# days rather than failures, and everything downstream skips with this step.
|
|
180
|
+
on_timeout: skip
|
|
181
|
+
config:
|
|
182
|
+
uri: ${steps.dates.output.value.store}/${steps.dates.output.value.previous_day}.json
|
|
183
|
+
|
|
184
|
+
recalled:
|
|
185
|
+
block: storage.read
|
|
186
|
+
depends_on: [seen]
|
|
187
|
+
config:
|
|
188
|
+
# The path the sensor above just found, read as the value yesterday's run wrote.
|
|
189
|
+
source: ${steps.seen.output.uri}
|
|
190
|
+
|
|
191
|
+
previous:
|
|
192
|
+
block: transform.jq
|
|
193
|
+
depends_on: [recalled]
|
|
194
|
+
config:
|
|
195
|
+
input: ${steps.recalled.output.value}
|
|
196
|
+
# Only the ids: the comparison is about identity, and yesterday's alert levels are
|
|
197
|
+
# yesterday's problem.
|
|
198
|
+
program: |
|
|
199
|
+
[.[].id]
|
|
200
|
+
|
|
201
|
+
fresh:
|
|
202
|
+
block: transform.jq
|
|
203
|
+
depends_on: [today, previous]
|
|
204
|
+
config:
|
|
205
|
+
input:
|
|
206
|
+
today: ${steps.today.output.value}
|
|
207
|
+
seen: ${steps.previous.output.value}
|
|
208
|
+
# A revised event keeps its id and moves its datemodified, so identity is the only safe
|
|
209
|
+
# key. An escalation from Green to Red is therefore not news here; a new eventid is.
|
|
210
|
+
program: |
|
|
211
|
+
. as {$today, $seen}
|
|
212
|
+
| [$today[] | select(.id as $id | $seen | index($id) | not)]
|
|
213
|
+
|
|
214
|
+
notify:
|
|
215
|
+
block: webhook.post
|
|
216
|
+
depends_on: [dates, fresh]
|
|
217
|
+
config:
|
|
218
|
+
url: ${params.webhook_url}
|
|
219
|
+
body:
|
|
220
|
+
kind: gdacs-updates
|
|
221
|
+
day: ${steps.dates.output.value.day}
|
|
222
|
+
events: ${steps.fresh.output.value}
|
|
223
|
+
|
|
224
|
+
triggers:
|
|
225
|
+
schedules:
|
|
226
|
+
- code: daily
|
|
227
|
+
name: Every morning, for the day that closed
|
|
228
|
+
description: Reads the previous day, which is the window this firing carries.
|
|
229
|
+
cron: "0 7 * * *"
|
|
230
|
+
timezone: UTC
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# New releases of a repository, relayed onward, with the rate limit treated as a fact of life.
|
|
2
|
+
#
|
|
3
|
+
# GitHub's REST API serves public repositories without a token: GET /repos/{owner}/{repo}/releases
|
|
4
|
+
# answers a list, newest first, each release carrying `tag_name`, `name`, `published_at`,
|
|
5
|
+
# `prerelease`, `draft` and `html_url`. Unauthenticated calls are limited to 60 requests per hour
|
|
6
|
+
# per IP, and that is the whole budget -- shared with everything else on that address. A schedule
|
|
7
|
+
# firing every ten minutes spends 6 of them; one firing hourly spends 1, which is the right
|
|
8
|
+
# cadence for something that publishes a release a month. A token raises the limit to 5000, and
|
|
9
|
+
# is the only reason to give this pipeline a connection.
|
|
10
|
+
#
|
|
11
|
+
# The floor is a parameter here, and the marker is written but not read. `since` says what has
|
|
12
|
+
# already been relayed, the last step records the newest stamp this run saw, and pointing the
|
|
13
|
+
# next firing's `since` at that marker is the read half of the pattern -- the same sensor-plus-
|
|
14
|
+
# transform shape any marker read has. Splitting it this way keeps the two halves legible: this
|
|
15
|
+
# document is about what "new" means and what a rate limit costs, not about state.
|
|
16
|
+
#
|
|
17
|
+
# What happens, hop by hop:
|
|
18
|
+
#
|
|
19
|
+
# releases one GET, with the API version pinned by the Accept header. GitHub asks every
|
|
20
|
+
# client for a User-Agent and refuses requests without one.
|
|
21
|
+
# candidates the releases flattened, each row carrying the floor and the two exclusions it
|
|
22
|
+
# will be judged by. A jq program never has values spliced into its text, so
|
|
23
|
+
# anything a filter compares against has to arrive as data on the row.
|
|
24
|
+
# fresh filter.jq: published after the floor, not a draft, and not a prerelease unless
|
|
25
|
+
# the run asked for them. Three comparisons, each between two fields of the row.
|
|
26
|
+
# digest the rows a person would read, with the carried limits dropped again.
|
|
27
|
+
# ordered the digest in publication order, which is the order the file below holds.
|
|
28
|
+
# staged storage.write, the digest as one json object. A converter reads a URI and writes
|
|
29
|
+
# a URI, so a value the run is holding is put down before it can be converted.
|
|
30
|
+
# written json to ndjson, because an empty list has to encode as an empty file for the
|
|
31
|
+
# gate below to see the difference.
|
|
32
|
+
# matched the gate: nothing new is a skip, not a failure, and the relay skips with it.
|
|
33
|
+
# relay the digest, posted. Postman Echo by default so this runs for real.
|
|
34
|
+
# marker the newest stamp seen, plus what the rate limit had left, computed under
|
|
35
|
+
# rule: all_done so a quiet run still records where it got to. GitHub reports the
|
|
36
|
+
# remaining budget in a response header, which is worth keeping beside the stamp:
|
|
37
|
+
# when the relay goes quiet, that number says whether it is the repo or the quota.
|
|
38
|
+
# remember that marker written to storage, which is the half the next firing would read.
|
|
39
|
+
#
|
|
40
|
+
# To make it yours: set repo, move `since` forward to when you started caring, and point
|
|
41
|
+
# webhook_url at your receiver. For a private repository or a heavier cadence, put a token in
|
|
42
|
+
# a connection and give the step `connection:` instead of `url:`.
|
|
43
|
+
#
|
|
44
|
+
# dg run --local examples/open-data/github-releases-relay.yaml
|
|
45
|
+
# dg run --local examples/open-data/github-releases-relay.yaml -p repo=jqlang/jq -p since=2030-01-01T00:00:00Z
|
|
46
|
+
|
|
47
|
+
format: dirigent/v1
|
|
48
|
+
kind: pipeline
|
|
49
|
+
code: github-releases-relay
|
|
50
|
+
name: GitHub releases relay
|
|
51
|
+
description: |
|
|
52
|
+
Releases of a public repository published since a floor, read from **GitHub's REST API**
|
|
53
|
+
without a token, and relayed onward -- only when there are any.
|
|
54
|
+
|
|
55
|
+
Unauthenticated calls share a budget of 60 requests an hour per IP, which is what decides
|
|
56
|
+
the cadence a schedule can have.
|
|
57
|
+
|
|
58
|
+
tags: [open-data, http, sensor, storage, transform, webhook, filter, relay, starter]
|
|
59
|
+
|
|
60
|
+
requires:
|
|
61
|
+
blocks:
|
|
62
|
+
- http.request
|
|
63
|
+
- transform.jq
|
|
64
|
+
- filter.jq
|
|
65
|
+
- map.jq
|
|
66
|
+
- convert.std
|
|
67
|
+
- storage.write
|
|
68
|
+
- storage.exists
|
|
69
|
+
- webhook.post
|
|
70
|
+
|
|
71
|
+
params:
|
|
72
|
+
type: object
|
|
73
|
+
additionalProperties: false
|
|
74
|
+
properties:
|
|
75
|
+
repo:
|
|
76
|
+
type: string
|
|
77
|
+
default: jqlang/jq
|
|
78
|
+
description: owner/name of a public repository.
|
|
79
|
+
since:
|
|
80
|
+
type: string
|
|
81
|
+
default: "2020-01-01T00:00:00Z"
|
|
82
|
+
description: Only releases published after this instant are relayed.
|
|
83
|
+
include_prereleases:
|
|
84
|
+
type: boolean
|
|
85
|
+
default: false
|
|
86
|
+
per_page:
|
|
87
|
+
type: integer
|
|
88
|
+
default: 30
|
|
89
|
+
maximum: 100
|
|
90
|
+
description: One page is one request against a 60-per-hour budget; ask for enough.
|
|
91
|
+
webhook_url:
|
|
92
|
+
type: string
|
|
93
|
+
default: https://postman-echo.com/post
|
|
94
|
+
|
|
95
|
+
steps:
|
|
96
|
+
releases:
|
|
97
|
+
block: http.request
|
|
98
|
+
config:
|
|
99
|
+
url: https://api.github.com/repos/${params.repo}/releases
|
|
100
|
+
headers:
|
|
101
|
+
# Pinning the API version here is what stops a future default from reshaping the
|
|
102
|
+
# payload under a pipeline that has been running for a year.
|
|
103
|
+
Accept: application/vnd.github+json
|
|
104
|
+
X-GitHub-Api-Version: "2022-11-28"
|
|
105
|
+
User-Agent: dirigent-example
|
|
106
|
+
query:
|
|
107
|
+
per_page: ${params.per_page}
|
|
108
|
+
|
|
109
|
+
candidates:
|
|
110
|
+
block: transform.jq
|
|
111
|
+
depends_on: [releases]
|
|
112
|
+
config:
|
|
113
|
+
input:
|
|
114
|
+
releases: ${steps.releases.output.body}
|
|
115
|
+
since: ${params.since}
|
|
116
|
+
include_prereleases: ${params.include_prereleases}
|
|
117
|
+
# A draft has no published_at at all, so the null is replaced with a stamp that loses
|
|
118
|
+
# every comparison rather than being compared as null.
|
|
119
|
+
program: |
|
|
120
|
+
. as {$since, $include_prereleases}
|
|
121
|
+
| [.releases[]
|
|
122
|
+
| {
|
|
123
|
+
tag: .tag_name,
|
|
124
|
+
title: (.name // .tag_name),
|
|
125
|
+
published_at: (.published_at // "0000-01-01T00:00:00Z"),
|
|
126
|
+
draft: .draft,
|
|
127
|
+
prerelease: .prerelease,
|
|
128
|
+
url: .html_url,
|
|
129
|
+
since: $since,
|
|
130
|
+
include_prereleases: $include_prereleases
|
|
131
|
+
}]
|
|
132
|
+
|
|
133
|
+
fresh:
|
|
134
|
+
block: filter.jq
|
|
135
|
+
depends_on: [candidates]
|
|
136
|
+
config:
|
|
137
|
+
input: ${steps.candidates.output.value}
|
|
138
|
+
# ISO 8601 stamps in the same zone compare correctly as strings, which is the one thing
|
|
139
|
+
# that format is designed to make true.
|
|
140
|
+
program: |
|
|
141
|
+
.published_at > .since
|
|
142
|
+
and (.draft | not)
|
|
143
|
+
and (.include_prereleases or (.prerelease | not))
|
|
144
|
+
|
|
145
|
+
digest:
|
|
146
|
+
block: map.jq
|
|
147
|
+
depends_on: [fresh]
|
|
148
|
+
config:
|
|
149
|
+
input: ${steps.fresh.output.value}
|
|
150
|
+
program: |
|
|
151
|
+
del(.since, .include_prereleases)
|
|
152
|
+
|
|
153
|
+
ordered:
|
|
154
|
+
block: transform.jq
|
|
155
|
+
depends_on: [digest]
|
|
156
|
+
config:
|
|
157
|
+
input: ${steps.digest.output.value}
|
|
158
|
+
program: |
|
|
159
|
+
sort_by(.published_at)
|
|
160
|
+
|
|
161
|
+
staged:
|
|
162
|
+
block: storage.write
|
|
163
|
+
depends_on: [ordered]
|
|
164
|
+
config:
|
|
165
|
+
target: ${run.scratch}/fresh.json
|
|
166
|
+
value: ${steps.ordered.output.value}
|
|
167
|
+
|
|
168
|
+
written:
|
|
169
|
+
block: convert.std
|
|
170
|
+
depends_on: [staged]
|
|
171
|
+
config:
|
|
172
|
+
source: ${steps.staged.output.uri}
|
|
173
|
+
target: ${run.scratch}/fresh.ndjson
|
|
174
|
+
from: json
|
|
175
|
+
to: ndjson
|
|
176
|
+
|
|
177
|
+
matched:
|
|
178
|
+
block: storage.exists
|
|
179
|
+
depends_on: [written]
|
|
180
|
+
poll: 1s
|
|
181
|
+
deadline: 3s
|
|
182
|
+
on_timeout: skip
|
|
183
|
+
config:
|
|
184
|
+
uri: ${steps.written.output.target}
|
|
185
|
+
min_size: 1b
|
|
186
|
+
|
|
187
|
+
relay:
|
|
188
|
+
block: webhook.post
|
|
189
|
+
depends_on: [digest, matched]
|
|
190
|
+
config:
|
|
191
|
+
url: ${params.webhook_url}
|
|
192
|
+
body:
|
|
193
|
+
kind: github-releases
|
|
194
|
+
repo: ${params.repo}
|
|
195
|
+
since: ${params.since}
|
|
196
|
+
releases: ${steps.digest.output.value}
|
|
197
|
+
|
|
198
|
+
marker:
|
|
199
|
+
block: transform.jq
|
|
200
|
+
depends_on: [releases, digest, matched]
|
|
201
|
+
# all_done: a run that relayed nothing still saw the repository, and still knows what the
|
|
202
|
+
# budget had left.
|
|
203
|
+
rule: all_done
|
|
204
|
+
config:
|
|
205
|
+
input:
|
|
206
|
+
repo: ${params.repo}
|
|
207
|
+
releases: ${steps.digest.output.value}
|
|
208
|
+
# Header names arrive lower-cased, which is what HTTP/2 makes of them.
|
|
209
|
+
rate_limit_remaining: ${steps.releases.output.headers.x-ratelimit-remaining}
|
|
210
|
+
# The newest stamp seen, which is what the next run's `since` should be. `max` over an
|
|
211
|
+
# empty list is null, so a quiet run reports null rather than inventing a floor.
|
|
212
|
+
program: |
|
|
213
|
+
{
|
|
214
|
+
repo,
|
|
215
|
+
rate_limit_remaining,
|
|
216
|
+
relayed: (.releases | length),
|
|
217
|
+
newest_published_at: ([.releases[].published_at] | max)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
remember:
|
|
221
|
+
block: storage.write
|
|
222
|
+
depends_on: [marker]
|
|
223
|
+
config:
|
|
224
|
+
target: ${run.scratch}/markers/releases.json
|
|
225
|
+
value: ${steps.marker.output.value}
|