Skip to content

Upgrade Cargo Dependencies #14

Upgrade Cargo Dependencies

Upgrade Cargo Dependencies #14

name: Upgrade Cargo Dependencies
on:
schedule:
# 5th and 20th at 9am Chicago time (15:00 UTC)
- cron: '0 15 5,20 * *'
workflow_dispatch: # Allow manual trigger
permissions:
contents: write
issues: write
pull-requests: write
jobs:
cargo-upgrade:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Verify opencode submodule pin is published
run: ./scripts/check-opencode-submodule-published.sh
- name: Install Linux system dependencies
run: |
# Required by opencode-broker (libpam-sys links against -lpam).
sudo apt-get update
sudo apt-get install -y libpam0g-dev
- name: Initialize opencode submodule
run: |
git -c url."https://github.com/".insteadOf=git@github.com: submodule update --init --recursive packages/opencode
git submodule status --recursive
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.89"
components: rustfmt, clippy
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '20'
- name: Setup Bun
uses: ./.github/actions/setup-bun
with:
install: "false"
- name: Rust cache
uses: Swatinem/rust-cache@v2
- name: Install cargo-edit
run: cargo install cargo-edit --locked
- name: Install cargo-audit
run: cargo install cargo-audit --locked
- name: Install just
uses: extractions/setup-just@v3
- name: Run cargo upgrade, test, and audit
id: upgrade
run: |
set +e
set -o pipefail
updates_dir="${RUNNER_TEMP}"
strip_ansi() {
sed -E 's/\x1B\[[0-9;]*[A-Za-z]//g' "$1" > "$2"
}
upgrade_output_path="${updates_dir}/cargo-upgrade-output.txt"
upgrade_output_plain_path="${updates_dir}/cargo-upgrade-output-plain.txt"
install_output_path="${updates_dir}/bun-install-output.txt"
install_output_plain_path="${updates_dir}/bun-install-output-plain.txt"
format_fix_output_path="${updates_dir}/cargo-fmt-fix-output.txt"
format_fix_output_plain_path="${updates_dir}/cargo-fmt-fix-output-plain.txt"
clippy_fix_output_path="${updates_dir}/cargo-clippy-fix-output.txt"
clippy_fix_output_plain_path="${updates_dir}/cargo-clippy-fix-output-plain.txt"
format_output_path="${updates_dir}/cargo-fmt-output.txt"
format_output_plain_path="${updates_dir}/cargo-fmt-output-plain.txt"
clippy_output_path="${updates_dir}/cargo-clippy-output.txt"
clippy_output_plain_path="${updates_dir}/cargo-clippy-output-plain.txt"
build_output_path="${updates_dir}/cargo-build-output.txt"
build_output_plain_path="${updates_dir}/cargo-build-output-plain.txt"
test_output_path="${updates_dir}/cargo-test-output.txt"
test_output_plain_path="${updates_dir}/cargo-test-output-plain.txt"
core_build_output_path="${updates_dir}/core-build-output.txt"
core_build_output_plain_path="${updates_dir}/core-build-output-plain.txt"
verify_output_path="${updates_dir}/cli-verify-output.txt"
verify_output_plain_path="${updates_dir}/cli-verify-output-plain.txt"
audit_output_path="${updates_dir}/cargo-audit-output.txt"
audit_output_plain_path="${updates_dir}/cargo-audit-output-plain.txt"
upgrade_exit=0
upgrade_cmd=(cargo upgrade --incompatible)
if cargo upgrade --help | grep -q -- '--workspace'; then
"${upgrade_cmd[@]}" --workspace 2>&1 | tee "${upgrade_output_path}"
upgrade_status=$?
if [ "${upgrade_status}" -ne 0 ]; then
upgrade_exit="${upgrade_status}"
fi
else
"${upgrade_cmd[@]}" 2>&1 | tee "${upgrade_output_path}"
upgrade_status=$?
if [ "${upgrade_status}" -ne 0 ]; then
upgrade_exit="${upgrade_status}"
fi
"${upgrade_cmd[@]}" --manifest-path packages/core/Cargo.toml 2>&1 | tee -a "${upgrade_output_path}"
upgrade_status=$?
if [ "${upgrade_status}" -ne 0 ]; then
upgrade_exit="${upgrade_status}"
fi
"${upgrade_cmd[@]}" --manifest-path packages/cli-rust/Cargo.toml 2>&1 | tee -a "${upgrade_output_path}"
upgrade_status=$?
if [ "${upgrade_status}" -ne 0 ]; then
upgrade_exit="${upgrade_status}"
fi
fi
strip_ansi "${upgrade_output_path}" "${upgrade_output_plain_path}"
if git diff --quiet; then
echo "updates_available=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "updates_available=true" >> $GITHUB_OUTPUT
install_exit=0
format_fix_exit=0
clippy_fix_exit=0
format_exit=0
clippy_exit=0
build_exit=0
test_exit=0
core_build_exit=0
verify_exit=0
audit_exit=0
if [ "${upgrade_exit}" -eq 0 ]; then
bun install --frozen-lockfile --ignore-scripts 2>&1 | tee "${install_output_path}"
install_exit=$?
strip_ansi "${install_output_path}" "${install_output_plain_path}"
cargo fmt --all 2>&1 | tee "${format_fix_output_path}"
format_fix_status=$?
if [ "${format_fix_status}" -ne 0 ]; then
format_fix_exit="${format_fix_status}"
fi
strip_ansi "${format_fix_output_path}" "${format_fix_output_plain_path}"
cargo clippy --all-targets --all-features --fix --allow-dirty --allow-staged 2>&1 | tee "${clippy_fix_output_path}"
clippy_fix_status=$?
if [ "${clippy_fix_status}" -ne 0 ]; then
clippy_fix_exit="${clippy_fix_status}"
fi
strip_ansi "${clippy_fix_output_path}" "${clippy_fix_output_plain_path}"
cargo fmt --all 2>&1 | tee -a "${format_fix_output_path}"
format_fix_status=$?
if [ "${format_fix_status}" -ne 0 ]; then
format_fix_exit="${format_fix_status}"
fi
strip_ansi "${format_fix_output_path}" "${format_fix_output_plain_path}"
# Project checks should run via shared just targets.
just check-rust-format 2>&1 | tee "${format_output_path}"
format_exit=$?
strip_ansi "${format_output_path}" "${format_output_plain_path}"
just check-rust-clippy 2>&1 | tee "${clippy_output_path}"
clippy_exit=$?
strip_ansi "${clippy_output_path}" "${clippy_output_plain_path}"
just build-rust 2>&1 | tee "${build_output_path}"
build_exit=$?
strip_ansi "${build_output_path}" "${build_output_plain_path}"
just ci-test 2>&1 | tee "${test_output_path}"
test_exit=$?
strip_ansi "${test_output_path}" "${test_output_plain_path}"
just build-core-bindings 2>&1 | tee "${core_build_output_path}"
core_build_exit=$?
strip_ansi "${core_build_output_path}" "${core_build_output_plain_path}"
just ci-verify 2>&1 | tee "${verify_output_path}"
verify_exit=$?
strip_ansi "${verify_output_path}" "${verify_output_plain_path}"
cargo audit 2>&1 | tee "${audit_output_path}"
audit_exit=$?
strip_ansi "${audit_output_path}" "${audit_output_plain_path}"
else
echo "Skipping checks because cargo upgrade exited with ${upgrade_exit}." | tee "${install_output_path}"
echo "Skipping checks because cargo upgrade exited with ${upgrade_exit}." | tee "${format_fix_output_path}"
echo "Skipping checks because cargo upgrade exited with ${upgrade_exit}." | tee "${clippy_fix_output_path}"
echo "Skipping checks because cargo upgrade exited with ${upgrade_exit}." | tee "${format_output_path}"
echo "Skipping checks because cargo upgrade exited with ${upgrade_exit}." | tee "${clippy_output_path}"
echo "Skipping checks because cargo upgrade exited with ${upgrade_exit}." | tee "${build_output_path}"
echo "Skipping checks because cargo upgrade exited with ${upgrade_exit}." | tee "${test_output_path}"
echo "Skipping checks because cargo upgrade exited with ${upgrade_exit}." | tee "${core_build_output_path}"
echo "Skipping checks because cargo upgrade exited with ${upgrade_exit}." | tee "${verify_output_path}"
echo "Skipping checks because cargo upgrade exited with ${upgrade_exit}." | tee "${audit_output_path}"
strip_ansi "${install_output_path}" "${install_output_plain_path}"
strip_ansi "${format_fix_output_path}" "${format_fix_output_plain_path}"
strip_ansi "${clippy_fix_output_path}" "${clippy_fix_output_plain_path}"
strip_ansi "${format_output_path}" "${format_output_plain_path}"
strip_ansi "${clippy_output_path}" "${clippy_output_plain_path}"
strip_ansi "${build_output_path}" "${build_output_plain_path}"
strip_ansi "${test_output_path}" "${test_output_plain_path}"
strip_ansi "${core_build_output_path}" "${core_build_output_plain_path}"
strip_ansi "${verify_output_path}" "${verify_output_plain_path}"
strip_ansi "${audit_output_path}" "${audit_output_plain_path}"
fi
checks_passed=true
if [ "${upgrade_exit}" -ne 0 ] || \
[ "${install_exit}" -ne 0 ] || \
[ "${format_exit}" -ne 0 ] || \
[ "${clippy_exit}" -ne 0 ] || \
[ "${build_exit}" -ne 0 ] || \
[ "${test_exit}" -ne 0 ] || \
[ "${core_build_exit}" -ne 0 ] || \
[ "${verify_exit}" -ne 0 ] || \
[ "${audit_exit}" -ne 0 ]; then
checks_passed=false
fi
echo "upgrade_exit=${upgrade_exit}" >> $GITHUB_OUTPUT
echo "install_exit=${install_exit}" >> $GITHUB_OUTPUT
echo "format_fix_exit=${format_fix_exit}" >> $GITHUB_OUTPUT
echo "clippy_fix_exit=${clippy_fix_exit}" >> $GITHUB_OUTPUT
echo "format_exit=${format_exit}" >> $GITHUB_OUTPUT
echo "clippy_exit=${clippy_exit}" >> $GITHUB_OUTPUT
echo "build_exit=${build_exit}" >> $GITHUB_OUTPUT
echo "test_exit=${test_exit}" >> $GITHUB_OUTPUT
echo "core_build_exit=${core_build_exit}" >> $GITHUB_OUTPUT
echo "verify_exit=${verify_exit}" >> $GITHUB_OUTPUT
echo "audit_exit=${audit_exit}" >> $GITHUB_OUTPUT
echo "checks_passed=${checks_passed}" >> $GITHUB_OUTPUT
echo "upgrade_output_plain_path=${upgrade_output_plain_path}" >> $GITHUB_OUTPUT
echo "install_output_plain_path=${install_output_plain_path}" >> $GITHUB_OUTPUT
echo "format_fix_output_plain_path=${format_fix_output_plain_path}" >> $GITHUB_OUTPUT
echo "clippy_fix_output_plain_path=${clippy_fix_output_plain_path}" >> $GITHUB_OUTPUT
echo "format_output_plain_path=${format_output_plain_path}" >> $GITHUB_OUTPUT
echo "clippy_output_plain_path=${clippy_output_plain_path}" >> $GITHUB_OUTPUT
echo "build_output_plain_path=${build_output_plain_path}" >> $GITHUB_OUTPUT
echo "test_output_plain_path=${test_output_plain_path}" >> $GITHUB_OUTPUT
echo "core_build_output_plain_path=${core_build_output_plain_path}" >> $GITHUB_OUTPUT
echo "verify_output_plain_path=${verify_output_plain_path}" >> $GITHUB_OUTPUT
echo "audit_output_plain_path=${audit_output_plain_path}" >> $GITHUB_OUTPUT
exit 0
- name: Prepare PR body
if: steps.upgrade.outputs.updates_available == 'true'
id: pr
run: |
body_path="${RUNNER_TEMP}/cargo-upgrade.md"
cat > "${body_path}" << 'HEADER'
## Cargo dependency upgrades
This PR was automatically generated by the scheduled cargo upgrade workflow.
### Changes
HEADER
echo '```diff' >> "${body_path}"
git diff >> "${body_path}"
echo '```' >> "${body_path}"
cat >> "${body_path}" << EOF
### Cargo Upgrade Output
\`\`\`
$(cat "${{ steps.upgrade.outputs.upgrade_output_plain_path }}")
\`\`\`
EOF
test_status="passed"
audit_status="passed"
if [ "${{ steps.upgrade.outputs.test_exit }}" != "0" ]; then
test_status="failed"
fi
if [ "${{ steps.upgrade.outputs.audit_exit }}" != "0" ]; then
audit_status="failed"
fi
cat >> "${body_path}" << EOF
### Test Result
Status: **${test_status}**
\`\`\`
$(cat "${{ steps.upgrade.outputs.test_output_plain_path }}")
\`\`\`
### Audit Result
Status: **${audit_status}**
\`\`\`
$(cat "${{ steps.upgrade.outputs.audit_output_plain_path }}")
\`\`\`
EOF
cat >> "${body_path}" << 'FOOTER'
### Testing
- [ ] CI passed
---
*Generated by [cargo-upgrades workflow](https://github.com/${{ github.repository }}/actions/workflows/cargo-upgrades.yml)*
FOOTER
echo "body_path=${body_path}" >> $GITHUB_OUTPUT
- name: Create issue on upgrade failure
if: steps.upgrade.outputs.updates_available == 'true' && steps.upgrade.outputs.checks_passed != 'true'
uses: actions/github-script@v7
env:
UPGRADE_OUTPUT: ${{ steps.upgrade.outputs.upgrade_output_plain_path }}
INSTALL_OUTPUT: ${{ steps.upgrade.outputs.install_output_plain_path }}
FORMAT_FIX_OUTPUT: ${{ steps.upgrade.outputs.format_fix_output_plain_path }}
CLIPPY_FIX_OUTPUT: ${{ steps.upgrade.outputs.clippy_fix_output_plain_path }}
FORMAT_OUTPUT: ${{ steps.upgrade.outputs.format_output_plain_path }}
CLIPPY_OUTPUT: ${{ steps.upgrade.outputs.clippy_output_plain_path }}
BUILD_OUTPUT: ${{ steps.upgrade.outputs.build_output_plain_path }}
TEST_OUTPUT: ${{ steps.upgrade.outputs.test_output_plain_path }}
CORE_BUILD_OUTPUT: ${{ steps.upgrade.outputs.core_build_output_plain_path }}
VERIFY_OUTPUT: ${{ steps.upgrade.outputs.verify_output_plain_path }}
AUDIT_OUTPUT: ${{ steps.upgrade.outputs.audit_output_plain_path }}
UPGRADE_EXIT: ${{ steps.upgrade.outputs.upgrade_exit }}
INSTALL_EXIT: ${{ steps.upgrade.outputs.install_exit }}
FORMAT_FIX_EXIT: ${{ steps.upgrade.outputs.format_fix_exit }}
CLIPPY_FIX_EXIT: ${{ steps.upgrade.outputs.clippy_fix_exit }}
FORMAT_EXIT: ${{ steps.upgrade.outputs.format_exit }}
CLIPPY_EXIT: ${{ steps.upgrade.outputs.clippy_exit }}
BUILD_EXIT: ${{ steps.upgrade.outputs.build_exit }}
TEST_EXIT: ${{ steps.upgrade.outputs.test_exit }}
CORE_BUILD_EXIT: ${{ steps.upgrade.outputs.core_build_exit }}
VERIFY_EXIT: ${{ steps.upgrade.outputs.verify_exit }}
AUDIT_EXIT: ${{ steps.upgrade.outputs.audit_exit }}
with:
script: |
const fs = require('fs');
const upgradeExit = process.env.UPGRADE_EXIT || '0';
const installExit = process.env.INSTALL_EXIT || '0';
const formatFixExit = process.env.FORMAT_FIX_EXIT || '0';
const clippyFixExit = process.env.CLIPPY_FIX_EXIT || '0';
const formatExit = process.env.FORMAT_EXIT || '0';
const clippyExit = process.env.CLIPPY_EXIT || '0';
const buildExit = process.env.BUILD_EXIT || '0';
const testExit = process.env.TEST_EXIT || '0';
const coreBuildExit = process.env.CORE_BUILD_EXIT || '0';
const verifyExit = process.env.VERIFY_EXIT || '0';
const auditExit = process.env.AUDIT_EXIT || '0';
const readFileSafe = (path) => {
try {
return fs.readFileSync(path, 'utf8');
} catch (error) {
return `Failed to read ${path}: ${error.message}`;
}
};
const tail = (text, maxChars) => {
if (!text || text.length <= maxChars) {
return text || '';
}
return text.slice(text.length - maxChars);
};
const upgradeOutput = readFileSafe(process.env.UPGRADE_OUTPUT);
const installOutput = readFileSafe(process.env.INSTALL_OUTPUT);
const formatFixOutput = readFileSafe(process.env.FORMAT_FIX_OUTPUT);
const clippyFixOutput = readFileSafe(process.env.CLIPPY_FIX_OUTPUT);
const formatOutput = readFileSafe(process.env.FORMAT_OUTPUT);
const clippyOutput = readFileSafe(process.env.CLIPPY_OUTPUT);
const buildOutput = readFileSafe(process.env.BUILD_OUTPUT);
const testOutput = readFileSafe(process.env.TEST_OUTPUT);
const coreBuildOutput = readFileSafe(process.env.CORE_BUILD_OUTPUT);
const verifyOutput = readFileSafe(process.env.VERIFY_OUTPUT);
const auditOutput = readFileSafe(process.env.AUDIT_OUTPUT);
const sections = [];
sections.push('## Cargo upgrade failed during CI');
sections.push('');
sections.push('### Summary');
sections.push(`- Upgrade exit: \`${upgradeExit}\``);
sections.push(`- Install exit: \`${installExit}\``);
sections.push(`- Format fix exit: \`${formatFixExit}\``);
sections.push(`- Clippy fix exit: \`${clippyFixExit}\``);
sections.push(`- Format exit: \`${formatExit}\``);
sections.push(`- Clippy exit: \`${clippyExit}\``);
sections.push(`- Build exit: \`${buildExit}\``);
sections.push(`- Test exit: \`${testExit}\``);
sections.push(`- Core bindings exit: \`${coreBuildExit}\``);
sections.push(`- CLI verify exit: \`${verifyExit}\``);
sections.push(`- Audit exit: \`${auditExit}\``);
sections.push('');
sections.push('### Run');
sections.push(`- ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`);
sections.push('');
sections.push('### Result');
sections.push('- PR not created because checks failed.');
sections.push('');
sections.push('### Cargo Upgrade Output (tail)');
sections.push('```');
sections.push(tail(upgradeOutput, 12000));
sections.push('```');
sections.push('');
if (installExit !== '0') {
sections.push('### Bun Install Output (tail)');
sections.push('```');
sections.push(tail(installOutput, 12000));
sections.push('```');
sections.push('');
}
if (formatFixExit !== '0') {
sections.push('### cargo fmt (fix) Output (tail)');
sections.push('```');
sections.push(tail(formatFixOutput, 12000));
sections.push('```');
sections.push('');
}
if (clippyFixExit !== '0') {
sections.push('### Clippy Fix Output (tail)');
sections.push('```');
sections.push(tail(clippyFixOutput, 12000));
sections.push('```');
sections.push('');
}
if (formatExit !== '0') {
sections.push('### cargo fmt Output (tail)');
sections.push('```');
sections.push(tail(formatOutput, 12000));
sections.push('```');
sections.push('');
}
if (clippyExit !== '0') {
sections.push('### Clippy Output (tail)');
sections.push('```');
sections.push(tail(clippyOutput, 12000));
sections.push('```');
sections.push('');
}
if (buildExit !== '0') {
sections.push('### Cargo Build Output (tail)');
sections.push('```');
sections.push(tail(buildOutput, 12000));
sections.push('```');
sections.push('');
}
if (testExit !== '0') {
sections.push('### Test Output (tail)');
sections.push('```');
sections.push(tail(testOutput, 12000));
sections.push('```');
sections.push('');
}
if (coreBuildExit !== '0') {
sections.push('### Core Bindings Build Output (tail)');
sections.push('```');
sections.push(tail(coreBuildOutput, 12000));
sections.push('```');
sections.push('');
}
if (verifyExit !== '0') {
sections.push('### CLI Verify Output (tail)');
sections.push('```');
sections.push(tail(verifyOutput, 12000));
sections.push('```');
sections.push('');
}
if (auditExit !== '0') {
sections.push('### Audit Output (tail)');
sections.push('```');
sections.push(tail(auditOutput, 12000));
sections.push('```');
sections.push('');
}
const title = `cargo upgrade failed in CI (${new Date().toISOString().slice(0, 10)})`;
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body: sections.join('\\n'),
labels: ['dependencies', 'ci'],
});
- name: Create Pull Request
if: steps.upgrade.outputs.updates_available == 'true' && steps.upgrade.outputs.checks_passed == 'true'
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
title: "chore(deps): cargo upgrade"
body-path: ${{ steps.pr.outputs.body_path }}
branch: cargo-dependency-upgrades
commit-message: "chore(deps): cargo upgrade"
labels: dependencies,security
delete-branch: true
add-paths: |
Cargo.lock
Cargo.toml
packages/core/Cargo.toml
packages/cli-rust/Cargo.toml