Skip to content
Open
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
10 changes: 10 additions & 0 deletions .github/.dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,13 @@ updates:
prefix: "Go"
reviewers:
- "TykTechnologies/engineering"

- package-ecosystem: gomod
directory: /eol-notifier
schedule:
interval: weekly
commit-message:
include: scope
prefix: "Go"
reviewers:
- "TykTechnologies/engineering"
67 changes: 67 additions & 0 deletions .github/eol-notifier/dependencies.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Dependencies tracked by .github/workflows/eol-notifier.yaml.
#
# Each entry maps a dependency to an endoflife.date product slug. Several
# entries may share a slug: services the API does not track ride on the
# lifecycle of their upstream OSS engine and are marked upstream_proxy.
---
thresholds_months: [12, 6, 1]

dependencies:
# Upstream OSS engines, tracked directly.
- name: Redis
product: redis
track: [eol]
- name: Valkey
product: valkey
track: [eol, eoas]
- name: PostgreSQL
product: postgresql
track: [eol]
- name: MongoDB
product: mongodb
track: [eol]
- name: MySQL
product: mysql
track: [eol, eoas]
- name: MariaDB
product: mariadb
track: [eol, eoes]

# HashiCorp tooling. A version gets its end-of-life date on the day a newer
# release drops it out of support, so the date is never in the future: expect
# new-version alerts from these, not the 12/6/1 month countdown.
- name: HashiCorp Vault
product: hashicorp-vault
track: [eol]
- name: HashiCorp Consul
product: consul
track: [eol]

# Managed services with their own published lifecycle.
- name: Amazon ElastiCache (Redis)
product: amazon-elasticache-redis
track: [eol, eoes]
- name: Amazon RDS PostgreSQL
product: amazon-rds-postgresql
track: [eol, eoes]
- name: Amazon DocumentDB
product: amazon-documentdb
track: [eol, eoes]
- name: Azure Database for PostgreSQL
product: azure-database-for-postgresql
track: [eol]

# Not tracked by endoflife.date. The upstream engine stands in for them, so
# the reported dates are indicative and need confirming with the provider.
- name: GCP MemoryStore
product: redis
track: [eol]
upstream_proxy: true
- name: GCP Cloud SQL
product: postgresql
track: [eol]
upstream_proxy: true
- name: Azure DocumentDB
product: mongodb
track: [eol]
upstream_proxy: true
18 changes: 18 additions & 0 deletions .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ on:
paths:
- 'branch-suggestion/**'
- 'jira-linter/**'
- 'eol-notifier/**'
- '.github/workflows/ci-test.yml'
pull_request:
paths:
- 'branch-suggestion/**'
- 'jira-linter/**'
- 'eol-notifier/**'
- '.github/workflows/ci-test.yml'

jobs:
Expand Down Expand Up @@ -64,3 +66,19 @@ jobs:
- name: Run tests
working-directory: jira-linter
run: go test ./...

test-eol-notifier:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: '1.24.7'

- name: Run tests
working-directory: eol-notifier
run: go test ./...
116 changes: 116 additions & 0 deletions .github/workflows/eol-notifier.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: Dependency EoL Notifier

on:
schedule:
- cron: '0 7 * * *' # Daily at 07:00 UTC
workflow_dispatch:
inputs:
dry_run:
description: 'Render the digest in the job log without posting to Slack'
type: boolean
default: false

# The recorded state lives on its own branch, so nothing is pushed to main.
env:
STATE_BRANCH: eol-notifier-state
STATE_REF: refs/eol-notifier/previous
STATE_FILE: state.json

# Two runs would build their commits on the same parent, and the second push
# would be rejected, so a manual run waits for the scheduled one to finish.
concurrency:
group: eol-notifier-state
cancel-in-progress: false

permissions:
contents: write

jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Restore recorded release cycles
run: |
# A missing branch and an unreachable origin must not look the same:
# treating a network failure as "no state yet" would suppress every
# new-version alert and then try to push a parentless commit.
set +e
git ls-remote --exit-code --heads origin "$STATE_BRANCH" > /dev/null
status=$?
set -e

case "$status" in
0)
git fetch origin "+refs/heads/$STATE_BRANCH:$STATE_REF"
if git cat-file -p "$STATE_REF:$STATE_FILE" > "$RUNNER_TEMP/$STATE_FILE"; then
echo "--> restored state from $STATE_BRANCH"
else
rm -f "$RUNNER_TEMP/$STATE_FILE"
echo "::warning::$STATE_BRANCH has no $STATE_FILE, recording a baseline"
fi
;;
2)
echo "--> no $STATE_BRANCH branch yet, this run records a baseline"
;;
*)
echo "::error::could not reach origin to look up $STATE_BRANCH"
exit 1
;;
esac

- name: Check dependency lifecycles
id: notifier
uses: ./eol-notifier
with:
config-path: .github/eol-notifier/dependencies.yaml
state-path: ${{ runner.temp }}/state.json
slack-webhook-url: ${{ secrets.EOL_SLACK_WEBHOOK_URL }}
dry-run: ${{ inputs.dry_run || false }}

# Runs even when the step above failed. A failed product fetch still posts
# the rest of the digest and still writes state, so skipping this would
# re-announce the same versions tomorrow. When nothing was written the file
# is unchanged and no commit is made.
- name: Record observed release cycles
if: ${{ !inputs.dry_run && !cancelled() }}
env:
GIT_AUTHOR_NAME: github-actions[bot]
GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
GIT_COMMITTER_NAME: github-actions[bot]
GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
run: |
if [ ! -f "$RUNNER_TEMP/$STATE_FILE" ]; then
echo "--> no state was written, nothing to record"
exit 0
fi

# main requires a reviewed PR, so the state cannot be committed there.
# The commit is assembled with plumbing and pushed straight to the state
# branch, which leaves the checked-out working tree alone and needs no
# second worktree.
blob=$(git hash-object -w "$RUNNER_TEMP/$STATE_FILE")

parent=""
if git rev-parse --verify --quiet "$STATE_REF" > /dev/null; then
parent=$(git rev-parse "$STATE_REF")
previous=$(git rev-parse "$STATE_REF:$STATE_FILE" 2> /dev/null || true)
if [ "$previous" = "$blob" ]; then
echo "--> state unchanged, nothing to commit"
exit 0
fi
fi

tree=$(printf '100644 blob %s\t%s\n' "$blob" "$STATE_FILE" | git mktree)

message="chore(eol-notifier): record observed release cycles"
if [ -n "$parent" ]; then
commit=$(git commit-tree "$tree" -p "$parent" -m "$message")
else
commit=$(git commit-tree "$tree" -m "$message")
fi

git push origin "$commit:refs/heads/$STATE_BRANCH"
echo "--> recorded state as $commit on $STATE_BRANCH"
107 changes: 107 additions & 0 deletions docs/workflows/eol-notifier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
## Dependency EoL Notifier

A scheduled job that reads the [endoflife.date](https://endoflife.date) API every
day at 07:00 UTC and posts one message to Slack. The message goes to the channel
of the webhook held in `EOL_SLACK_WEBHOOK_URL`, which is `#service-EoL-dependency`.

The job sends two kinds of alert:

- **Approaching end of life** — a tracked version reaches the end of a lifecycle
phase in exactly 12, 6 or 1 month.
- **New versions** — the API lists a version that the run before did not see.

When there is nothing to report, the job posts no message at all.

Acting on an alert is manual work. Adding a version to the test matrix, or taking
one out of it, needs a PR against this repository. For a removal, the dependency
policy asks for commercial approval first.

### Configuration

The tracked dependencies are listed in
[.github/eol-notifier/dependencies.yaml](/.github/eol-notifier/dependencies.yaml).
Each entry maps one dependency to an endoflife.date product slug:

```yaml
thresholds_months: [12, 6, 1]

dependencies:
- name: Amazon RDS PostgreSQL
product: amazon-rds-postgresql
track: [eol, eoes]

- name: GCP Cloud SQL
product: postgresql
upstream_proxy: true
```

`thresholds_months` sets how long before the end date an alert goes out. It is
optional, and `[12, 6, 1]` is the default.

`track` selects the lifecycle phases to watch: `eol`, `eoas` (active support) or
`eoes` (extended support). It is optional and defaults to `[eol]`. Not every
product publishes every phase.

`upstream_proxy` marks a service that endoflife.date does not track, such as GCP
MemoryStore or Azure DocumentDB. For those, the job follows the upstream engine
instead, so the date is only a hint. The alert marks the entry, and the footer of
the message says the date has to be confirmed with the cloud provider.

The job reads this file before it calls the API. An unknown phase name, an empty
product or a repeated dependency name stops the run before any request is sent.

### Recorded state

To know that a version is *new*, the job needs to know what the run before it
saw. It keeps that in a `state.json` file on a branch of its own, called
`eol-notifier-state`. It is a branch and not a file on `main`, because `main`
needs a reviewed PR and the job cannot push there.

The branch holds that one file and nothing else. The job commits only when the
content changes, so expect a few commits a year, not one a day. It creates the
branch itself on the first run.

If you delete the branch, the next run starts from zero. It records the versions
the API lists that day and reports none of them as new. Versions that appeared
while the branch was gone become part of that new record, so they are never
announced. A dependency added to the config behaves the same way: its first run
only records.

End-of-life alerts do not use the state file. They are also the part with no
second chance. The alert goes out on the single day that is exactly 12, 6 or 1
month before the end date, which is what the policy asks for, so an alert is lost
when the run of that day is skipped or fails. The next threshold still fires, so
a missed 12-month alert is followed by the 6-month one. If you expected an alert
and it never arrived, check the run history of the workflow.

### Products that give no notice

Some vendors set the end date only once it has arrived. HashiCorp gives a Vault or
Consul version its end-of-life date on the day a newer release pushes it out of
support: for Vault that is the next release, for Consul the third one after it.
The date is that same day, so it is never in the future and there is nothing to
count down from. For those two products the 12, 6 and 1 month alerts never fire.

The new-version alert covers them instead. A new release means an older one lost
support that day, so check the older versions when such a message arrives.

### Failures

A product the API does not return is logged and skipped. The job still posts the
alerts for the other products, and then fails, so the problem shows up in the
Actions tab. The state file is still written in that case. Without that, the same
new versions would be announced again the next day.

### Requirements

The repository secret `EOL_SLACK_WEBHOOK_URL` must hold an incoming webhook for
the target channel. The job cannot post without it.

### Manual runs

Start the workflow by hand with `workflow_dispatch`. With `dry_run: true` it
writes the message into the job log, posts nothing to Slack and leaves the state
file alone. A dry run shows only what is due on that day. It does not report the
lifecycle of every tracked product.

Adoption: Internal use for the scheduled job on this repository.
1 change: 1 addition & 0 deletions eol-notifier/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/notifier
Loading
Loading