From f25f58d393a08d3b5863a8fe4d33af056875f138 Mon Sep 17 00:00:00 2001 From: Zoey Rose Date: Sun, 11 Oct 2026 21:00:29 -0500 Subject: [PATCH 1/6] ci: report dependency advisories without blocking merges --- .github/workflows/dependency-review.yml | 10 +++++++--- AGENTS.md | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index be6aec2..e619586 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -10,6 +10,7 @@ on: permissions: contents: read + pull-requests: write jobs: dependency-review: @@ -17,12 +18,15 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 10 steps: + # Advisory findings inform the implementer; findings and operational errors do not block CI. - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + continue-on-error: true with: - fail-on-severity: high + fail-on-severity: low fail-on-scopes: runtime,development,unknown - warn-only: false - comment-summary-in-pr: never + warn-only: true + license-check: false + comment-summary-in-pr: always show-openssf-scorecard: false retry-on-snapshot-warnings: true retry-on-snapshot-warnings-timeout: 120 diff --git a/AGENTS.md b/AGENTS.md index 6ee0d21..5291bd8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,3 +44,7 @@ approval, retry, rollback, and offline-agent behavior covered by deterministic tests. Do not enable a live target or add an official-server target merely by merging a code change; those are separate operator-approved provisioning and authorization steps. + +Dependency review is advisory. Before merge, the Codex implementer reads its PR +comment or job summary, fixes actionable introduced dependency risk, or records +explicit maintainer acceptance of existing risk. From 82fe1b052a2f7164786bea5c5c9b142e75cca2e5 Mon Sep 17 00:00:00 2001 From: Zoey Rose Date: Sun, 11 Oct 2026 21:26:26 -0500 Subject: [PATCH 2/6] ci: keep dependency advisory reports current and visible --- .github/workflows/dependency-review.yml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index e619586..eeaa49f 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -3,11 +3,13 @@ name: Dependency review on: pull_request: types: [opened, synchronize, reopened, edited] - branches: - - main merge_group: types: [checks_requested] +concurrency: + group: dependency-review-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + permissions: contents: read pull-requests: write @@ -20,13 +22,25 @@ jobs: steps: # Advisory findings inform the implementer; findings and operational errors do not block CI. - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + id: dependency_review continue-on-error: true with: fail-on-severity: low fail-on-scopes: runtime,development,unknown warn-only: true - license-check: false + license-check: true comment-summary-in-pr: always show-openssf-scorecard: false retry-on-snapshot-warnings: true retry-on-snapshot-warnings-timeout: 120 + - name: Report advisory review failures + if: always() && steps.dependency_review.outcome == 'failure' + continue-on-error: true + shell: bash + run: | + echo "::warning::Dependency review reported a failure and remains advisory. Review available reports, annotations and logs; absent reports leave vulnerability and license results unknown." + { + echo "## Dependency review needs attention" + echo + echo "Action reported a failure; remains advisory. Review available report, annotations and logs; if report absent vulnerability/license results unknown." + } >> "$GITHUB_STEP_SUMMARY" From 43857de4c609897c0b7161ed87565edfc64cdd95 Mon Sep 17 00:00:00 2001 From: Zoey Rose Date: Sun, 11 Oct 2026 21:53:48 -0500 Subject: [PATCH 3/6] ci: report dependency advice on pull requests --- .github/workflows/dependency-review.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index eeaa49f..a48a952 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -3,8 +3,6 @@ name: Dependency review on: pull_request: types: [opened, synchronize, reopened, edited] - merge_group: - types: [checks_requested] concurrency: group: dependency-review-${{ github.event.pull_request.number || github.ref }} From b57a958a778bcfb026308a07e0c42196f855cf84 Mon Sep 17 00:00:00 2001 From: Zoey Rose Date: Sun, 11 Oct 2026 22:28:33 -0500 Subject: [PATCH 4/6] ci: refresh advisory reports and current-head guidance --- .github/workflows/dependency-review.yml | 167 ++++++++++++++++++++++-- AGENTS.md | 9 +- 2 files changed, 164 insertions(+), 12 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index a48a952..d130aeb 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -18,14 +18,18 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - # Advisory findings inform the implementer; findings and operational errors do not block CI. - - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + # Advisories and operational errors are nonblocking; read the latest current-head run summary. + # Fork/Dependabot comment restrictions fall back to the job summary. + - name: Review dependency changes id: dependency_review + timeout-minutes: 5 continue-on-error: true + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 with: fail-on-severity: low fail-on-scopes: runtime,development,unknown warn-only: true + # License metadata only; repository license compatibility rules still apply. license-check: true comment-summary-in-pr: always show-openssf-scorecard: false @@ -34,11 +38,156 @@ jobs: - name: Report advisory review failures if: always() && steps.dependency_review.outcome == 'failure' continue-on-error: true - shell: bash + timeout-minutes: 2 + env: + GITHUB_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + shell: python run: | - echo "::warning::Dependency review reported a failure and remains advisory. Review available reports, annotations and logs; absent reports leave vulnerability and license results unknown." - { - echo "## Dependency review needs attention" - echo - echo "Action reported a failure; remains advisory. Review available report, annotations and logs; if report absent vulnerability/license results unknown." - } >> "$GITHUB_STEP_SUMMARY" + import json + import os + import re + import signal + import urllib.error + import urllib.request + + MARKER = '' + NOTICE = ('Dependency review needs attention: the action reported a failure. ' + 'Inspect the available report, annotations, and logs. ' + 'An absent report means the result is unknown, not clean.') + + + def summary(text): + print('::warning::' + text, flush=True) + with open(os.environ['GITHUB_STEP_SUMMARY'], 'a', encoding='utf-8') as stream: + stream.write('\n' + text + '\n') + + + class Stop(Exception): + pass + + + class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + raise Stop('API redirect refused; PR comment was not refreshed.') + + + def report(): + summary(NOTICE) # Preserve visible evidence before any API operation. + env = os.environ + repo, number, head = env['GITHUB_REPOSITORY'], env['PR_NUMBER'], env['PR_HEAD_SHA'] + run, attempt = env['GITHUB_RUN_ID'], env['GITHUB_RUN_ATTEMPT'] + if (env['GITHUB_API_URL'] != 'https://api.github.com' + or env['GITHUB_SERVER_URL'] != 'https://github.com' + or env['GITHUB_EVENT_NAME'] != 'pull_request' + or not re.fullmatch(r'atrinik/[A-Za-z0-9_.-]+', repo) + or not all(re.fullmatch(r'[1-9][0-9]*', value) for value in (number, run, attempt)) + or not re.fullmatch(r'[0-9a-f]{40}', head)): + raise Stop('Invalid reporting context; PR comment was not refreshed.') + run_url = f'https://github.com/{repo}/actions/runs/{run}/attempts/{attempt}' + body = (f'## Dependency review needs attention\n\n{NOTICE}\n\n' + f'PR head: `{head}`\n\n[Current run summary and logs]({run_url})\n\n{MARKER}') + summary(f'Review head `{head}`: {run_url}. Always inspect the latest run for the current PR head.') + base = f'https://api.github.com/repos/{repo}' + comments_url = f'{base}/issues/{number}/comments' + opener = urllib.request.build_opener(NoRedirect()) + + def api(method, url, data=None): + request = urllib.request.Request(url, method=method, + data=None if data is None else json.dumps(data).encode('utf-8'), + headers={'Authorization': 'Bearer ' + env['GITHUB_TOKEN'], + 'Accept': 'application/vnd.github+json', + 'Content-Type': 'application/json', + 'X-GitHub-Api-Version': '2022-11-28'}) + with opener.open(request, timeout=10) as response: + raw = response.read(8 * 1024 * 1024 + 1) + if len(raw) > 8 * 1024 * 1024: + raise Stop('API response exceeded the reporting bound; PR comment was not refreshed.') + return json.loads(raw), response.headers.get('Link', '') + + def owned(comment): + return (comment['user']['login'] == 'github-actions[bot]' + and comment['user']['type'] == 'Bot' + and comment['body'].count(MARKER) == 1 + and comment['body'].rstrip().endswith(MARKER)) + + repository, _ = api('GET', base) + repository_id = repository['id'] + if (repository['full_name'] != repo + or type(repository_id) is not int or repository_id <= 0): + raise Stop('Repository identity mismatch; PR comment was not refreshed.') + numeric_comments_url = f'https://api.github.com/repositories/{repository_id}/issues/{number}/comments' + comment_routes = (comments_url, numeric_comments_url) + candidates, seen = [], set() + for page in range(1, 11): + comments, links = api('GET', f'{comments_url}?per_page=100&page={page}') + if not isinstance(comments, list) or len(comments) > 100: + raise Stop('Invalid comment pagination; PR comment was not refreshed.') + relations = {} + for link in links.split(',') if links else []: + match = re.fullmatch(r'\s*<([^>]+)>;\s*rel="(next|prev|first|last)"\s*', link) + if not match or match[2] in relations: + raise Stop('Invalid comment pagination; PR comment was not refreshed.') + if not any(re.fullmatch(re.escape(route) + r'\?per_page=100&page=[1-9][0-9]*', match[1]) for route in comment_routes): + raise Stop('Unexpected pagination destination; PR comment was not refreshed.') + relations[match[2]] = match[1] + for relation, destination in relations.items(): + target_page = int(destination.rsplit('=', 1)[1]) + if ((relation == 'first' and target_page != 1) + or (relation == 'prev' and target_page != page - 1) + or (relation == 'last' and (target_page < page or (target_page > page and 'next' not in relations)))): + raise Stop('Inconsistent comment pagination; PR comment was not refreshed.') + if 'next' in relations and (len(comments) != 100 or int(relations['next'].rsplit('=', 1)[1]) != page + 1): + raise Stop('Inconsistent comment pagination; PR comment was not refreshed.') + for comment in comments: + cid = comment['id'] + if type(cid) is not int or cid <= 0 or cid in seen or not isinstance(comment['body'], str): + raise Stop('Invalid or repeated comment; PR comment was not refreshed.') + seen.add(cid) + if MARKER in comment['body']: + if not owned(comment): + raise Stop('Ambiguous dependency review marker; PR comment was not refreshed.') + candidates.append(comment) + # A full page always requires another request, even without Link. + if len(comments) < 100: + break + else: + raise Stop('Comment pagination bound reached; PR comment was not refreshed.') + if len(candidates) > 1: + raise Stop('Duplicate dependency review comments; PR comment was not refreshed.') + if candidates: + candidate = candidates[0] + target = f"{base}/issues/comments/{candidate['id']}" + fresh, _ = api('GET', target) + if fresh != candidate or not owned(fresh): + raise Stop('Dependency review comment changed; PR comment was not refreshed.') + fresh_pr, _ = api('GET', f'{base}/pulls/{number}') + if (fresh_pr['number'] != int(number) + or fresh_pr['base']['repo']['full_name'] != repo + or fresh_pr['head']['sha'] != head): + raise Stop('PR head or base changed; stale run did not refresh the PR comment.') + api('PATCH' if candidates else 'POST', target if candidates else comments_url, {'body': body}) + + + def main(): + def deadline(signum, frame): + raise Stop('Reporting deadline reached; inspect the run summary for current evidence.') + signal.signal(signal.SIGALRM, deadline) + signal.alarm(90) + try: + report() + except Stop as error: + summary(str(error)) # Only fixed, locally defined messages. + except urllib.error.HTTPError as error: + error.close() + summary('PR comment could not be refreshed (API permission denied).' if error.code == 403 + else 'PR comment could not be refreshed (API request failed).') + except Exception: + summary('PR comment could not be refreshed (reporting or network error). Inspect the current run summary.') + finally: + signal.alarm(0) + + + if __name__ == '__main__': + main() diff --git a/AGENTS.md b/AGENTS.md index 5291bd8..c70b6df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,6 +45,9 @@ tests. Do not enable a live target or add an official-server target merely by merging a code change; those are separate operator-approved provisioning and authorization steps. -Dependency review is advisory. Before merge, the Codex implementer reads its PR -comment or job summary, fixes actionable introduced dependency risk, or records -explicit maintainer acceptance of existing risk. +Dependency review is advisory. Before merge, the Codex implementer reads the +latest current-HEAD job summary even when a PR comment exists, fixes actionable +introduced dependency risk, or records explicit maintainer acceptance of existing +risk. Coverage requires the workflow to exist on the relevant PR/merge revision. +License metadata does not certify compatibility; existing repository license +policies still apply. From 5bbfa39eaca0b3856745bbe02db69c66e5b0595f Mon Sep 17 00:00:00 2001 From: Zoey Rose Date: Sun, 11 Oct 2026 23:01:04 -0500 Subject: [PATCH 5/6] ci: fence advisory failure reports to the current revision --- .github/workflows/dependency-review.yml | 26 +++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index d130aeb..23b738a 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -43,6 +43,8 @@ jobs: GITHUB_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} shell: python run: | import json @@ -78,12 +80,16 @@ jobs: env = os.environ repo, number, head = env['GITHUB_REPOSITORY'], env['PR_NUMBER'], env['PR_HEAD_SHA'] run, attempt = env['GITHUB_RUN_ID'], env['GITHUB_RUN_ATTEMPT'] + base_ref, base_sha = env['PR_BASE_REF'], env['PR_BASE_SHA'] if (env['GITHUB_API_URL'] != 'https://api.github.com' or env['GITHUB_SERVER_URL'] != 'https://github.com' or env['GITHUB_EVENT_NAME'] != 'pull_request' or not re.fullmatch(r'atrinik/[A-Za-z0-9_.-]+', repo) or not all(re.fullmatch(r'[1-9][0-9]*', value) for value in (number, run, attempt)) - or not re.fullmatch(r'[0-9a-f]{40}', head)): + or not re.fullmatch(r'[0-9a-f]{40}', head) + or not re.fullmatch(r'[0-9a-f]{40}', base_sha) + or not 1 <= len(base_ref) <= 1024 + or re.search(r'[\x00-\x1f\x7f-\x9f]', base_ref)): raise Stop('Invalid reporting context; PR comment was not refreshed.') run_url = f'https://github.com/{repo}/actions/runs/{run}/attempts/{attempt}' body = (f'## Dependency review needs attention\n\n{NOTICE}\n\n' @@ -160,11 +166,27 @@ jobs: candidate = candidates[0] target = f"{base}/issues/comments/{candidate['id']}" fresh, _ = api('GET', target) - if fresh != candidate or not owned(fresh): + + def identity(comment): + user = comment['user'] + if (type(comment['id']) is not int or comment['id'] <= 0 + or not isinstance(comment['body'], str) + or type(user['id']) is not int or user['id'] <= 0 + or user['login'] != 'github-actions[bot]' or user['type'] != 'Bot' + or not isinstance(comment['updated_at'], str) + or not re.fullmatch(r'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z', comment['updated_at']) + or comment['issue_url'] not in tuple(route.removesuffix('/comments') for route in comment_routes)): + raise Stop('Invalid dependency review comment identity; PR comment was not refreshed.') + return (comment['id'], comment['body'], user['id'], user['login'], + user['type'], comment['updated_at'], comment['issue_url']) + + if identity(fresh) != identity(candidate) or not owned(fresh): raise Stop('Dependency review comment changed; PR comment was not refreshed.') fresh_pr, _ = api('GET', f'{base}/pulls/{number}') if (fresh_pr['number'] != int(number) or fresh_pr['base']['repo']['full_name'] != repo + or fresh_pr['base']['ref'] != base_ref + or fresh_pr['base']['sha'] != base_sha or fresh_pr['head']['sha'] != head): raise Stop('PR head or base changed; stale run did not refresh the PR comment.') api('PATCH' if candidates else 'POST', target if candidates else comments_url, {'body': body}) From a9179d13b23f46fcc8574df68f4c3eeca9d64bfe Mon Sep 17 00:00:00 2001 From: Zoey Rose Date: Sun, 11 Oct 2026 23:58:04 -0500 Subject: [PATCH 6/6] ci: stop advisory reporting when runs are cancelled --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 23b738a..d2ff903 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -36,7 +36,7 @@ jobs: retry-on-snapshot-warnings: true retry-on-snapshot-warnings-timeout: 120 - name: Report advisory review failures - if: always() && steps.dependency_review.outcome == 'failure' + if: ${{ !cancelled() && steps.dependency_review.outcome == 'failure' }} continue-on-error: true timeout-minutes: 2 env: