Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
- package-ecosystem: 'npm' # See documentation for possible values
directory: '/' # Location of package manifests
schedule:
interval: 'weekly'
21 changes: 21 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Release Please

on:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
release-please:
runs-on: ubuntu-latest
steps:
- name: Run Release Please
uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dist
node_modules
package-lock.json
__INTERNAL__
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "1.0.2"
}
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2025 Really Him
Copyright (c) 2026 Really Him

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
32 changes: 32 additions & 0 deletions MAINTAINERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Maintainers

This file documents the release workflow for this repository.

## Release automation (Release Please)

- Workflow: `.github/workflows/release-please.yml`
- Trigger: pushes to `main` (or manual run via `workflow_dispatch`)
- Behavior: opens or updates a release PR and manages `CHANGELOG.md`
- Merge: merging the release PR creates the Git tag and GitHub Release

## Release checklist

1. Ensure commit messages follow conventional commits:
- `fix:` for patch releases
- `feat:` for minor releases
- `feat!:` or `BREAKING CHANGE:` for major releases
2. Verify `dist/` is up to date if any `src/` files changed since the last release:
- run `npm run bundle`
- if `dist/` changes, commit them to the release PR
3. Confirm CI is green on the release PR
4. Merge the release PR

## After merge

- Confirm the GitHub Release is published (not a draft)
- Verify the Marketplace listing reflects the new release tag/version

## Notes

- `CHANGELOG.md` is generated by Release Please; avoid manual edits.
- If no release PR is created, confirm there are new conventional commits since the last tag.
40 changes: 25 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,27 @@ This action captures the rate-limit state at job start and compares it with the

## Usage

To use this action, just drop it in anywhere in your job - the pre- and post-job hooks will do all of the work.

```yaml
jobs:
track:
search:
runs-on: ubuntu-latest
outputs:
usage: ${{ steps.usage.outputs.usage }}
steps:
- uses: actions/checkout@v4
- uses: hesreallyhim/github-api-usage-tracker@v1
id: usage

report:
runs-on: ubuntu-latest
needs: track
steps:
- run: echo "Core API used: ${{ needs.track.outputs.usage }}"
- name Checkout
uses: actions/checkout@v4
- name Track Usage
uses: hesreallyhim/github-api-usage-tracker@v1
- name: Query API
uses: actions/github-script@v6
with:
script: |
const response = await ...
...
```

After your job completes, you'll get a nice summary:

<div align="center">
<img src="assets/flow-diagram.svg" alt="API Usage Tracking Flow" width="100%"/>
</div>
Expand Down Expand Up @@ -74,13 +77,20 @@ Example output:
- The action uses pre and post job hooks to snapshot the rate limit, so you only need to use it in one step - the rest will be handled automatically.
- Output is set in the post step, so it is only available after the job completes (use job outputs if needed).
- Logs are emitted via `core.debug()`. Enable step debug logging to view them.
- GitHub's primary rate limits appear to use fixed windows with reset times anchored to the first observed usage of the token (per resource bucket), rather than calendar-aligned rolling windows.”
• GitHub’s primary rate limit for Actions using the GITHUB_TOKEN is 1,000 REST API requests per hour per repository (or 15,000 per hour per repository when accessing GitHub Enterprise Cloud resources). This limit is specific to the automatically generated GITHUB_TOKEN and is independent of the standard REST API limits for other token types.
Reference: GitHub Actions limits documentation — “The rate limit for GITHUB_TOKEN is 1,000 requests per hour per repository.”
https://docs.github.com/en/actions/reference/limits
• When a GitHub Actions workflow uses a different token (such as a personal access token or a GitHub App installation token), the workflow is subject to that token’s normal primary API rate limits, not the GITHUB_TOKEN Actions limit (e.g., 5,000 requests per hour for a PAT).
Reference: GitHub REST API rate limits documentation
https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api

---

## License

<div align="center">

MIT © 2025 Really Him
## License

MIT © 2026 Really Him

</div>
154 changes: 139 additions & 15 deletions dist/post/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27862,7 +27862,79 @@ function makeSummaryTable(resources) {
return summaryTable;
}

module.exports = { formatMs, makeSummaryTable };
/**
* Computes usage stats for a single bucket using pre/post snapshots.
*
* @param {object} startingBucket - bucket from the pre snapshot.
* @param {object} endingBucket - bucket from the post snapshot.
* @param {number} endTimeSeconds - post snapshot time in seconds.
* @returns {object} usage details and validation status.
*/
function computeBucketUsage(startingBucket, endingBucket, endTimeSeconds) {
const result = {
valid: false,
used: 0,
remaining: undefined,
crossed_reset: false,
warnings: []
};

if (!startingBucket || !endingBucket) {
result.reason = 'missing_bucket';
return result;
}

const startingRemaining = Number(startingBucket.remaining);
const endingRemaining = Number(endingBucket.remaining);
if (!Number.isFinite(startingRemaining) || !Number.isFinite(endingRemaining)) {
result.reason = 'invalid_remaining';
return result;
}

const startingLimit = Number(startingBucket.limit);
const endingLimit = Number(endingBucket.limit);
const resetPre = Number(startingBucket.reset);
const crossedReset = Number.isFinite(resetPre) && endTimeSeconds >= resetPre;
result.crossed_reset = crossedReset;

let used;
if (crossedReset) {
if (!Number.isFinite(startingLimit) || !Number.isFinite(endingLimit)) {
result.reason = 'invalid_limit';
return result;
}
if (startingLimit !== endingLimit) {
result.warnings.push('limit_changed_across_reset');
}
used = startingLimit - startingRemaining + (endingLimit - endingRemaining);
} else {
if (
Number.isFinite(startingLimit) &&
Number.isFinite(endingLimit) &&
startingLimit !== endingLimit
) {
result.reason = 'limit_changed_without_reset';
return result;
}
used = startingRemaining - endingRemaining;
if (used < 0) {
result.reason = 'remaining_increased_without_reset';
return result;
}
}

if (used < 0) {
result.reason = 'negative_usage';
return result;
}

result.valid = true;
result.used = used;
result.remaining = endingRemaining;
return result;
}

module.exports = { formatMs, makeSummaryTable, computeBucketUsage };


/***/ }),
Expand Down Expand Up @@ -27980,7 +28052,7 @@ const fs = __nccwpck_require__(9896);
const path = __nccwpck_require__(6928);
const { fetchRateLimit } = __nccwpck_require__(5042);
const { log, parseBuckets } = __nccwpck_require__(9630);
const { formatMs, makeSummaryTable } = __nccwpck_require__(5828);
const { formatMs, makeSummaryTable, computeBucketUsage } = __nccwpck_require__(5828);

/**
* Writes JSON-stringified data to a file if a valid pathname is provided.
Expand Down Expand Up @@ -28032,6 +28104,7 @@ async function run() {
);
}
const endTime = Date.now();
const endTimeSeconds = Math.floor(endTime / 1000);
const duration = hasStartTime ? endTime - startTime : null;

log('[github-api-usage-tracker] Fetching final rate limits...');
Expand All @@ -28044,33 +28117,77 @@ async function run() {
log(`[github-api-usage-tracker] ${JSON.stringify(endingResources, null, 2)}`);

const data = {};
const crossedBuckets = [];
let totalUsed = 0;

for (const bucket of buckets) {
const startingUsed = startingResources[bucket]?.used;
const endingUsed = endingResources[bucket]?.used;
if (startingUsed === undefined) {
const startingBucket = startingResources[bucket];
const endingBucket = endingResources[bucket];
if (!startingBucket) {
core.warning(
`[github-api-usage-tracker] Starting rate limit bucket "${bucket}" not found; skipping`
);
continue;
}
if (endingUsed === undefined) {
if (!endingBucket) {
core.warning(
`[github-api-usage-tracker] Ending rate limit bucket "${bucket}" not found; skipping`
);
continue;
}
let used = endingUsed - startingUsed;
if (used < 0) {

const usage = computeBucketUsage(startingBucket, endingBucket, endTimeSeconds);
if (!usage.valid) {
switch (usage.reason) {
case 'invalid_remaining':
core.warning(
`[github-api-usage-tracker] Invalid remaining count for bucket "${bucket}"; skipping`
);
break;
case 'invalid_limit':
core.warning(
`[github-api-usage-tracker] Invalid limit for bucket "${bucket}" during reset crossing; skipping`
);
break;
case 'limit_changed_without_reset':
core.warning(
`[github-api-usage-tracker] Limit changed without reset for bucket "${bucket}"; skipping`
);
break;
case 'remaining_increased_without_reset':
core.warning(
`[github-api-usage-tracker] Remaining increased without reset for bucket "${bucket}"; skipping`
);
break;
case 'negative_usage':
core.warning(
`[github-api-usage-tracker] Negative usage for bucket "${bucket}" detected; skipping`
);
break;
default:
core.warning(
`[github-api-usage-tracker] Invalid usage data for bucket "${bucket}"; skipping`
);
break;
}
continue;
}

if (usage.warnings.includes('limit_changed_across_reset')) {
core.warning(
`[github-api-usage-tracker] Negative usage for bucket "${bucket}" detected; clamping to 0`
`[github-api-usage-tracker] Limit changed across reset for bucket "${bucket}"; results may reflect a token change`
);
used = 0;
}
const remaining = endingResources[bucket].remaining;
data[bucket] = { used, remaining };
totalUsed += used;

data[bucket] = {
used: usage.used,
remaining: usage.remaining,
crossed_reset: usage.crossed_reset
};
if (usage.crossed_reset) {
crossedBuckets.push(bucket);
}
totalUsed += usage.used;
}

// Set output
Expand All @@ -28088,9 +28205,16 @@ async function run() {
log(
`[github-api-usage-tracker] Preparing summary table for ${Object.keys(data).length} bucket(s)`
);
core.summary
const summary = core.summary
.addHeading('GitHub API Usage Tracker Summary')
.addTable(makeSummaryTable(data))
.addTable(makeSummaryTable(data));
if (crossedBuckets.length > 0) {
summary.addRaw(
`<p><strong>Reset Window Crossed:</strong> Yes (${crossedBuckets.join(', ')})</p>`,
true
);
}
summary
.addRaw(
`<p><strong>Action Duration:</strong> ${
hasStartTime ? formatMs(duration) : 'Unknown (data missing)'
Expand Down
10 changes: 10 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"packages": {
".": {
"release-type": "simple",
"package-name": "github-api-usage-tracker",
"changelog-path": "CHANGELOG.md",
"include-v-in-tag": true
}
}
}
Loading