Repository navigation
ci(decisions): reconcile the Current Decisions pages with a scheduled job #3795
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
migmartri
wants to merge
3
commits into
chainloop-dev:main
Choose a base branch
from
migmartri:ci/decisions-reconcile
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,328 @@ | ||
| # ============================================================================= | ||
| # Reconcile of the Current Decisions pages (docs/decisions/) | ||
| # ============================================================================= | ||
| # Runs the `eng-adr reconcile` skill of chainloop-dev/brain with Claude Code. | ||
| # Runs on weekdays, and on a manual start. When the pages change, it opens or updates one draft PR. | ||
| # | ||
| # The skill runs in its scheduled mode: it asks no questions, writes only the | ||
| # changes that need no person, and lists the open questions in the PR body. | ||
| # | ||
| # Claude edits files only. It gets read-only tokens. This workflow commits, | ||
| # pushes and opens the PR with the chainloop-eng GitHub App. The commits are | ||
| # signed by GitHub (sign-commits), signed off (DCO), and have an Assisted-by trailer. | ||
| # | ||
| # The parent tickets of the specs are GitHub issues of this repository. Claude | ||
| # reads them, and the pull requests, with the read-only job token. | ||
| # | ||
| # A run that cannot read a source (GitHub, the brain plugin) fails. It never | ||
| # reports "no change": "Check sources" runs before Claude starts, and | ||
| # "Check Claude run" checks the source errors and that Claude finished after. | ||
| # | ||
| # Required secrets: CHAINLOOP_ENG_APP_PRIVATE_KEY | ||
| # Required variables: ANTHROPIC_FEDERATION_RULE_ID | ||
| # | ||
| # Claude API access uses workload identity federation, not an API key. The | ||
| # federation IDs in the "Run eng-adr reconcile" step are not secrets. The | ||
| # federation rule must match only this repository on main: | ||
| # subject_prefix: repo:chainloop-dev/chainloop:ref:refs/heads/main | ||
| # claims: repository_owner=chainloop-dev, workflow=Reconcile Current Decisions | ||
| # audience: https://api.anthropic.com | ||
| # The chainloop-eng app must be installed on chainloop-dev/chainloop | ||
| # (contents and pull requests: write) and chainloop-dev/brain (contents: read). | ||
| # ============================================================================= | ||
|
|
||
| name: Reconcile Current Decisions | ||
|
|
||
| on: | ||
| schedule: | ||
| # 05:00 UTC, Monday to Friday. | ||
| - cron: "0 5 * * 1-5" | ||
| workflow_dispatch: | ||
| inputs: | ||
| area: | ||
| description: "Reconcile only this area. Leave blank for all areas." | ||
| required: false | ||
| type: string | ||
| dry_run: | ||
| description: "Do not open a PR. Upload the diff and the PR body as an artifact. Clear it to open the PR." | ||
| required: false | ||
| default: true | ||
| type: boolean | ||
|
|
||
| permissions: {} | ||
|
|
||
| concurrency: | ||
| group: decisions-reconcile | ||
| cancel-in-progress: false | ||
|
|
||
| env: | ||
| # Client ID of the chainloop-eng app (chainloop-dev/brain config/release.yaml). GitHub accepts the client ID | ||
| # in place of the numeric App ID to sign the app JWT, so it goes in the app-id input. | ||
| CHAINLOOP_ENG_APP_CLIENT_ID: Iv23liu5FUQ9TuCCdxpP | ||
| PR_BRANCH: decisions-reconcile-scheduled | ||
| # A scheduled run opens a PR. A manual run opens a PR only when dry_run is cleared. | ||
| DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} | ||
|
|
||
| jobs: | ||
| reconcile: | ||
| # Forks have no access to the secrets, the GitHub App or the federation rule. | ||
| if: github.repository == 'chainloop-dev/chainloop' | ||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| # The parent tickets of the specs. | ||
| issues: read | ||
| # GitHub OIDC token for the Anthropic workload identity federation. | ||
| id-token: write | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 90 | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | ||
| with: | ||
| # The skill reads the git history (refresh commits, git log -S). | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
|
|
||
| - name: Generate brain read token | ||
| id: brain-token | ||
| uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2 | ||
| with: | ||
| app-id: ${{ env.CHAINLOOP_ENG_APP_CLIENT_ID }} | ||
| private-key: ${{ secrets.CHAINLOOP_ENG_APP_PRIVATE_KEY }} | ||
| owner: chainloop-dev | ||
| repositories: brain | ||
| permission-contents: read | ||
|
|
||
| - name: Fetch brain plugin | ||
| env: | ||
| BRAIN_TOKEN: ${{ steps.brain-token.outputs.token }} | ||
| run: | | ||
| # Outside the workspace, so that the plugin files are never part of the PR. | ||
| git clone --depth 1 \ | ||
|
migmartri marked this conversation as resolved.
Resolved
|
||
| "https://x-access-token:${BRAIN_TOKEN}@github.com/chainloop-dev/brain.git" \ | ||
| "$RUNNER_TEMP/brain" | ||
| echo "Brain commit: $(git -C "$RUNNER_TEMP/brain" rev-parse HEAD)" | ||
|
|
||
| - name: Check sources | ||
| # The skill needs each of these sources. Without one, a run can report "no change" on pages that are | ||
| # out of date. This step fails the job before Claude starts. | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| BRAIN_DIR: ${{ runner.temp }}/brain | ||
| run: | | ||
| fail() { echo "::error::$1"; exit 1; } | ||
|
|
||
| # GitHub. Claude, the collector and the skill read the issues and the pull requests with the job token. | ||
| issues=$(gh api "repos/${GITHUB_REPOSITORY}/issues?state=all&per_page=1" --jq 'length') || issues="" | ||
| [ "$issues" = "1" ] || fail "GitHub: the job token could not read the issues of ${GITHUB_REPOSITORY}." | ||
| echo "GitHub: read the issues of ${GITHUB_REPOSITORY}." | ||
| prs=$(gh api "repos/${GITHUB_REPOSITORY}/pulls?state=all&per_page=1" --jq 'length') || prs="" | ||
| [ "$prs" = "1" ] || fail "GitHub: the job token could not read the pull requests of ${GITHUB_REPOSITORY}." | ||
| echo "GitHub: read the pull requests of ${GITHUB_REPOSITORY}." | ||
|
|
||
| # Brain plugin. | ||
| [ -f "$BRAIN_DIR/skills/eng-adr/SKILL.md" ] || fail "Brain plugin: skills/eng-adr/SKILL.md does not exist." | ||
| specs=$(python3 "$BRAIN_DIR/skills/eng-adr/tools/decisions.py" --repo "$GITHUB_WORKSPACE" \ | ||
| | jq -r '.specs | length') || specs="" | ||
| [ -n "$specs" ] || fail "Brain plugin: the collector decisions.py did not run on the checkout." | ||
| echo "Brain plugin: the collector found ${specs} specs." | ||
|
|
||
| - name: Prepare reconcile run | ||
| id: prepare | ||
| env: | ||
| AREA: ${{ inputs.area }} | ||
| OUT_DIR: ${{ runner.temp }}/decisions | ||
| run: | | ||
| mkdir -p "$OUT_DIR" | ||
|
|
||
| { | ||
| echo "prompt<<PROMPT_EOF" | ||
| cat <<EOF | ||
| /br:eng-adr reconcile ${AREA} | ||
|
|
||
| This is an unattended run in GitHub Actions. Follow the scheduled-run rules of the skill: ask no questions, | ||
| write the changes that need no person, and list each open question in the PR body. | ||
|
|
||
| These changes need no person. Write them: | ||
| - Each result of "Re-check of a page entry" in the format doc that asks nobody. For example, a | ||
| "not confirmed" entry whose behavior the code check finds now becomes "verified" and moves to | ||
| Current decisions. | ||
| - A new reason on a Non-Confirmed entry whose ticket changed state. | ||
| - A new spec decision that both workers of "Worth an entry" pass. Its code state gives its section. | ||
| A decision that both workers fail goes on the "Not shown" line. | ||
| - A replacement that the replacement rules of the format doc settle with explicit evidence. | ||
| - The deciders from the collector, new IDs from next_ids, and the refresh line. | ||
| - A correction of "Check the written text" that does not change the meaning. | ||
| - The block of "Agent instructions". | ||
|
|
||
| These changes need a person. Do not write them. List each one as an open question in the PR body: | ||
| - A BORDER verdict of "Worth an entry". | ||
| - "A current decision that no longer holds". | ||
| - A decision that nothing in the repository can show ("user confirmed"). | ||
| - A change to the area map, for example an area for a spec in unmapped_specs. | ||
| - A change to the Language. | ||
| - A correction of "Check the written text" that changes the meaning. | ||
| - A conflict, and code that goes against a "Not shown" decision. | ||
|
|
||
| This workflow owns git and the PR. Change these steps of the skill: | ||
| - "Branch" and "Before you commit": stay on the current checkout of the default branch. Do not create or | ||
| switch branches. Do not commit, push, or open, edit or comment on a PR. | ||
| - "Show the user": there is no person to show the diff to. Do not file tickets. | ||
| - "PR": write the PR body to ${OUT_DIR}/pr-body.md, in the format of the "PR" step. Do not write a PR | ||
| title: this workflow sets it. No person read the final text, so the body lists what nobody reviewed, as | ||
| that step says. Write the GitHub handles of the reviewers of the "PR" step to | ||
| ${OUT_DIR}/reviewers.txt, one per line. | ||
| When nothing changed, write none of these files. | ||
|
|
||
| Sources: when a tool call to GitHub or the collector fails or is refused, stop. Do not continue with | ||
| less data, and do not report "no change". Write the tool, the call, and the error to | ||
| ${OUT_DIR}/source-error.txt. | ||
|
|
||
| Finish: the run ends when your turn ends. Wait for the result of each subagent before you end your turn. | ||
| As your last action, after the pages are written or you found that nothing changed, write | ||
| ${OUT_DIR}/done.txt. | ||
|
|
||
| Change only files under docs/decisions/ and the repository agent instructions file (CLAUDE.md). | ||
| EOF | ||
| echo "PROMPT_EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Run eng-adr reconcile | ||
| # The action SHA also pins the Claude Code CLI: v1.0.266 installs Claude Code 2.1.316. | ||
| # Bump the SHA deliberately, and check the CLI version in the run log. | ||
| uses: anthropics/claude-code-action@d535f5172dec02b204866b393d6b20b9b2aaf98d # v1.0.266 | ||
| env: | ||
| # Read-only. The collector looks up the PRs and the tickets of the decisions with it. | ||
| GH_TOKEN: ${{ github.token }} | ||
| # Subagents run in the foreground. A headless run ends with Claude's turn, so a background subagent's | ||
| # result is lost, and the run reports "no change". | ||
| CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" | ||
| with: | ||
| # Workload identity federation: the action exchanges the GitHub OIDC token for a short-lived | ||
| # Anthropic token. These are identifiers, not secrets. Do not also set anthropic_api_key. | ||
| # The federation rule is specific to this repository (see the header). | ||
| anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }} | ||
| anthropic_organization_id: 33e34a9b-ec6d-437d-ae3f-48eda06ae6d3 | ||
| anthropic_service_account_id: svac_01DNrcgtbfDfSTjmGkSpHGqR | ||
| anthropic_workspace_id: wrkspc_011fX5ZMoA7ZBqRG66mYGifU | ||
| # Read-only token, so the action does not mint a Claude GitHub App token with write access. | ||
| github_token: ${{ github.token }} | ||
| prompt: ${{ steps.prepare.outputs.prompt }} | ||
| # Shows Claude's final report in the job summary, also the open questions of a run that opens no PR. | ||
| display_report: true | ||
| claude_args: >- | ||
| --plugin-dir ${{ runner.temp }}/brain | ||
| --allowedTools "Bash,Read,Write,Edit,Glob,Grep,Agent,Task" | ||
|
|
||
| - name: Upload Claude output | ||
| # The action hides Claude's output in the log. Keep it after each run, to see what a run did and why. | ||
| if: ${{ always() }} | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||
| with: | ||
| name: claude-execution-output | ||
| path: ${{ runner.temp }}/claude-execution-output.json | ||
| if-no-files-found: warn | ||
| retention-days: 7 | ||
|
|
||
| - name: Check Claude run | ||
| env: | ||
| OUT_DIR: ${{ runner.temp }}/decisions | ||
| run: | | ||
| if [ -e "$OUT_DIR/source-error.txt" ]; then | ||
| echo "::error::Claude could not read a source. The run stopped:" | ||
| cat "$OUT_DIR/source-error.txt" | ||
| exit 1 | ||
| fi | ||
| if [ ! -e "$OUT_DIR/done.txt" ]; then | ||
| echo "::error::The Claude step ended before the reconcile finished (no done.txt)." | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Check changes | ||
| id: changes | ||
| env: | ||
| OUT_DIR: ${{ runner.temp }}/decisions | ||
| run: | | ||
| changed=$(git status --porcelain --untracked-files=all | awk '{print $2}') | ||
| unexpected=$(echo "$changed" | grep -vE '^(docs/decisions/|CLAUDE\.md$)' | grep -v '^$' || true) | ||
| if [ -n "$unexpected" ]; then | ||
| echo "::error::The reconcile run changed files outside docs/decisions/ and CLAUDE.md:" | ||
| echo "$unexpected" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [ -z "$changed" ]; then | ||
| echo "No change to the Current Decisions pages." | ||
| echo "changed=false" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
|
||
| if [ ! -s "$OUT_DIR/pr-body.md" ]; then | ||
| echo "::error::The pages changed, but the run wrote no PR body." | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Include new pages: mark untracked files as intent-to-add for the diff, then unstage them again. | ||
| git ls-files --others --exclude-standard -z | xargs -0 -r git add -N -- | ||
| git diff > "$OUT_DIR/changes.diff" | ||
| git reset -q | ||
| # A dated title tells the runs apart. Each run force-updates the PR, so the date shows the latest run. | ||
| title="adr: update $(date -u +%F)" | ||
| { | ||
| echo "title=${title}" | ||
| echo "reviewers=$(paste -sd, "$OUT_DIR/reviewers.txt" 2>/dev/null | tr -d '@ ')" | ||
| # AI disclosure (AI_POLICY.md): every commit that AI produced has an Assisted-by trailer. | ||
| echo "commit-message<<COMMIT_EOF" | ||
| printf '%s\n\nAssisted-by: Claude Code\n' "$title" | ||
| echo "COMMIT_EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
| echo "changed=true" >> "$GITHUB_OUTPUT" | ||
|
|
||
| { | ||
| echo "## ${title}" | ||
| cat "$OUT_DIR/pr-body.md" | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
|
|
||
| - name: Upload result (dry run) | ||
| if: ${{ env.DRY_RUN == 'true' && steps.changes.outputs.changed == 'true' }} | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||
| with: | ||
| name: decisions-reconcile | ||
| path: ${{ runner.temp }}/decisions/ | ||
|
|
||
| - name: Generate repository write token | ||
| if: ${{ env.DRY_RUN != 'true' && steps.changes.outputs.changed == 'true' }} | ||
| id: write-token | ||
| uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2 | ||
| with: | ||
| app-id: ${{ env.CHAINLOOP_ENG_APP_CLIENT_ID }} | ||
| private-key: ${{ secrets.CHAINLOOP_ENG_APP_PRIVATE_KEY }} | ||
| # Only this repository, also when the app is installed on other repositories. | ||
| repositories: chainloop | ||
| permission-contents: write | ||
| permission-pull-requests: write | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| - name: Open or update PR | ||
| if: ${{ env.DRY_RUN != 'true' && steps.changes.outputs.changed == 'true' }} | ||
| # Each run starts from the default branch and force-updates PR_BRANCH, so the PR always shows the latest | ||
| # reconcile. Do not push manual fixes to that branch: answer the open questions in a separate PR. | ||
| uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 | ||
| with: | ||
| token: ${{ steps.write-token.outputs.token }} | ||
| branch: ${{ env.PR_BRANCH }} | ||
| title: ${{ steps.changes.outputs.title }} | ||
| body-path: ${{ runner.temp }}/decisions/pr-body.md | ||
| commit-message: ${{ steps.changes.outputs.commit-message }} | ||
| signoff: true | ||
| # The repository requires signed commits. GitHub signs the commits that the app makes with the API. | ||
| sign-commits: true | ||
| add-paths: | | ||
| docs/decisions | ||
| CLAUDE.md | ||
| reviewers: ${{ steps.changes.outputs.reviewers }} | ||
| labels: | | ||
| automated | ||
| skip-ai-session | ||
| draft: true | ||
| delete-branch: true | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: The token steps pass a GitHub App client ID through
app-id, which expects the numeric App ID; token generation will fail before the brain or repository token is available. Pass this value throughclient-idin both steps, or configure the numeric App ID.Prompt for AI agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not changed. GitHub accepts the client ID in place of the numeric App ID as the JWT issuer, and the pinned action passes the app-id value through to it. The same step with the same value succeeds in chainloop-dev/platform (run 42352738620, step "Generate brain read token"). The workflow now has a comment that says this.