Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor 6888c3800a ci: verify the actions lockfile in CI
Fails the build when .github/workflows/actions.lock stops matching the
workflows, or when a locked pin no longer verifies upstream, so a stale or
tampered lockfile cannot go unnoticed between dependency updates.
2026-11-12 07:22:59 -05:00
Dylan M. Taylor 26d0ecba74 ci: lock workflow actions through an actions.lock lockfile
Mutable action tags are a supply-chain risk: a moved tag runs different code
without a diff. Every workflow action is now pinned through
.github/workflows/actions.lock, and CI verifies the lockfile so an out-of-sync
or tampered one fails instead of running.
2026-11-12 07:16:24 -05:00
13 changed files with 91 additions and 23 deletions
+31
View File
@@ -0,0 +1,31 @@
# This file is machine-generated by `gh actions-lock`.
# Do not edit by hand; run `gh actions-lock` to update.
# Docs: https://gh.io/actions-lockfile
version: 'v0.0.3'
workflows:
'.github/workflows/ci.yml':
- 'actions/checkout@v7.0.1'
- 'step-security/harden-runner@v2.22.4'
'.github/workflows/freebsd.yml': []
'.github/workflows/house-rules.yml': []
'.github/workflows/lockfile.yml':
- 'actions/checkout@v7.0.1'
- 'step-security/harden-runner@v2.22.4'
'.github/workflows/nix.yml': []
'.github/workflows/packaging-lint.yml':
- 'actions/checkout@v7.0.1'
- 'step-security/harden-runner@v2.22.4'
'.github/workflows/release.yml': []
'.github/workflows/sonarcloud.yml': []
'.github/workflows/windows-arm64.yml': []
dependencies:
'actions/checkout@v7.0.1':
ref: 'v7.0.1'
commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1'
owner_id: 44036562
repo_id: 197814629
'step-security/harden-runner@v2.22.4':
ref: 'v2.22.4'
commit: 'sha1-1d88384fd173af6ecb012dfd0264a306b711241a'
owner_id: 88700172
repo_id: 422287306
+2 -2
View File
@@ -27,11 +27,11 @@ jobs:
CRAFT_FONTS_REF: 37827adc41fd165f9a9234ac40f94dbad353aa69 CRAFT_FONTS_REF: 37827adc41fd165f9a9234ac40f94dbad353aa69
steps: steps:
- name: Harden runner - name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4 uses: step-security/harden-runner@v2.22.4
with: with:
egress-policy: audit egress-policy: audit
- name: Check out craft-fonts - name: Check out craft-fonts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@v7.0.1
with: with:
repository: storytold/craft-fonts repository: storytold/craft-fonts
ref: ${{ env.CRAFT_FONTS_REF }} ref: ${{ env.CRAFT_FONTS_REF }}
+34
View File
@@ -0,0 +1,34 @@
name: Lockfile
# Fails when .github/workflows/actions.lock is out of sync with the workflows, or when a locked pin
# no longer verifies upstream, so a moved tag cannot slip a different commit into a run.
# Docs: https://gh.io/actions-lockfile
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
env:
GH_TOKEN: ${{ github.token }}
jobs:
lockfile:
name: Lockfile is in sync
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Harden runner
uses: step-security/harden-runner@v2.22.4
with:
egress-policy: audit
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- name: Install gh-actions-lock
run: gh extension install github/gh-actions-lock --pin v0.4.1
- name: Verify the lockfile
run: gh actions-lock --verify
+2 -2
View File
@@ -28,10 +28,10 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- name: Harden runner - name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4 uses: step-security/harden-runner@v2.22.4
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: Install the pinned packaging toolchain - name: Install the pinned packaging toolchain
-3
View File
@@ -18,7 +18,4 @@ jobs:
sonarcloud: sonarcloud:
# Tracked at @main so org-wide CI improvements propagate without PR churn. # Tracked at @main so org-wide CI improvements propagate without PR churn.
uses: storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main uses: storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main
with:
# Keep coverage tests on the same reviewed, immutable font set as the main gate.
fonts-ref: 37827adc41fd165f9a9234ac40f94dbad353aa69
secrets: inherit secrets: inherit
+2 -2
View File
@@ -1,7 +1,7 @@
use std::process::{Command, Output}; use std::process::{Command, Output};
fn cli(args: &[&str]) -> Output { fn cli(args: &[&str]) -> Output {
Command::new(crate::deckcraft_cli()).args(args).output().unwrap() Command::new(env!("CARGO_BIN_EXE_deckcraft-cli")).args(args).output().unwrap()
} }
fn stdout(o: &Output) -> String { fn stdout(o: &Output) -> String {
@@ -197,7 +197,7 @@ fn every_subcommand_answers_help_and_does_nothing_else() {
std::fs::create_dir_all(&dir).unwrap(); std::fs::create_dir_all(&dir).unwrap();
for sub in SUBCOMMANDS { for sub in SUBCOMMANDS {
for args in [vec![sub, "--help"], vec![sub, "-h", "out.png"], vec!["help", sub]] { for args in [vec![sub, "--help"], vec![sub, "-h", "out.png"], vec!["help", sub]] {
let o = Command::new(crate::deckcraft_cli()).current_dir(&dir).args(&args).output().unwrap(); let o = Command::new(env!("CARGO_BIN_EXE_deckcraft-cli")).current_dir(&dir).args(&args).output().unwrap();
assert!(stdout(&o).starts_with(&format!("usage: deckcraft-cli {sub}")), "{args:?}"); assert!(stdout(&o).starts_with(&format!("usage: deckcraft-cli {sub}")), "{args:?}");
} }
} }
+2 -2
View File
@@ -29,7 +29,7 @@ fn closed_stdout() -> Stdio {
} }
fn run(dir: &Path, args: &[&str], stdout: Stdio) -> Output { fn run(dir: &Path, args: &[&str], stdout: Stdio) -> Output {
Command::new(crate::deckcraft_cli()).current_dir(dir).args(args).stdout(stdout).stderr(Stdio::piped()).output().unwrap() Command::new(env!("CARGO_BIN_EXE_deckcraft-cli")).current_dir(dir).args(args).stdout(stdout).stderr(Stdio::piped()).output().unwrap()
} }
#[test] #[test]
@@ -52,7 +52,7 @@ fn broken_pipe_does_not_skip_later_commands_or_save() {
assert!(out.status.success(), "exit {:?}: {}", out.status, String::from_utf8_lossy(&out.stderr)); assert!(out.status.success(), "exit {:?}: {}", out.status, String::from_utf8_lossy(&out.stderr));
let saved = dir.0.join("out.deckcraft"); let saved = dir.0.join("out.deckcraft");
assert!(saved.is_file(), "requested save was skipped"); assert!(saved.is_file(), "requested save was skipped");
let info = Command::new(crate::deckcraft_cli()).arg("info").arg(saved).output().unwrap(); let info = Command::new(env!("CARGO_BIN_EXE_deckcraft-cli")).arg("info").arg(saved).output().unwrap();
assert!(info.status.success(), "{}", String::from_utf8_lossy(&info.stderr)); assert!(info.status.success(), "{}", String::from_utf8_lossy(&info.stderr));
let data: serde_json::Value = serde_json::from_slice(&info.stdout).unwrap(); let data: serde_json::Value = serde_json::from_slice(&info.stdout).unwrap();
let slides = data["slides"].as_array().unwrap(); let slides = data["slides"].as_array().unwrap();
+3 -3
View File
@@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
use std::process::{Command, Output}; use std::process::{Command, Output};
fn cli(args: &[&Path]) -> Output { fn cli(args: &[&Path]) -> Output {
Command::new(crate::deckcraft_cli()).args(args).output().unwrap() Command::new(env!("CARGO_BIN_EXE_deckcraft-cli")).args(args).output().unwrap()
} }
fn temp_dir(name: &str) -> PathBuf { fn temp_dir(name: &str) -> PathBuf {
@@ -18,7 +18,7 @@ fn temp_dir(name: &str) -> PathBuf {
fn convert_rejects_output_formats_it_cannot_write() { fn convert_rejects_output_formats_it_cannot_write() {
let dir = temp_dir("convert-unsupported"); let dir = temp_dir("convert-unsupported");
let input = dir.join("in.pptx"); let input = dir.join("in.pptx");
let out = Command::new(crate::deckcraft_cli()).args(["run", "--sample", "--export"]).arg(&input).output().unwrap(); let out = Command::new(env!("CARGO_BIN_EXE_deckcraft-cli")).args(["run", "--sample", "--export"]).arg(&input).output().unwrap();
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
for name in ["out.odp", "out.bmp", "out.svg", "out.outline", "out"] { for name in ["out.odp", "out.bmp", "out.svg", "out.outline", "out"] {
@@ -48,7 +48,7 @@ fn run_rejects_unsupported_save_and_export_paths_before_running() {
let dir = temp_dir("run-unsupported"); let dir = temp_dir("run-unsupported");
for flag in ["--save", "--export"] { for flag in ["--save", "--export"] {
let target = dir.join("deck.odp"); let target = dir.join("deck.odp");
let out = Command::new(crate::deckcraft_cli()).args(["run", "--sample", flag]).arg(&target).output().unwrap(); let out = Command::new(env!("CARGO_BIN_EXE_deckcraft-cli")).args(["run", "--sample", flag]).arg(&target).output().unwrap();
assert!(!out.status.success(), "{flag}: run should fail"); assert!(!out.status.success(), "{flag}: run should fail");
assert!(String::from_utf8_lossy(&out.stderr).contains("unsupported output format .odp"), "{flag}"); assert!(String::from_utf8_lossy(&out.stderr).contains("unsupported output format .odp"), "{flag}");
assert!(!target.exists(), "{flag}: nothing is written"); assert!(!target.exists(), "{flag}: nothing is written");
+1 -1
View File
@@ -5,7 +5,7 @@ use std::path::PathBuf;
use std::process::{Command, Output}; use std::process::{Command, Output};
fn cli(args: &[&str]) -> Output { fn cli(args: &[&str]) -> Output {
Command::new(crate::deckcraft_cli()).args(args).output().unwrap() Command::new(env!("CARGO_BIN_EXE_deckcraft-cli")).args(args).output().unwrap()
} }
fn temp_dir(name: &str) -> PathBuf { fn temp_dir(name: &str) -> PathBuf {
-6
View File
@@ -1,5 +1,3 @@
use std::ffi::OsString;
mod cli; mod cli;
#[cfg(unix)] #[cfg(unix)]
mod closed_stdout; mod closed_stdout;
@@ -8,7 +6,3 @@ mod damaged;
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md). // Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md).
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] #[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod mcp_roots; mod mcp_roots;
fn deckcraft_cli() -> OsString {
std::env::var_os("NEXTEST_BIN_EXE_deckcraft_cli").unwrap_or_else(|| env!("CARGO_BIN_EXE_deckcraft-cli").into())
}
+1 -1
View File
@@ -7,7 +7,7 @@ use std::process::{Command, Output, Stdio};
use serde_json::Value; use serde_json::Value;
fn bin() -> Command { fn bin() -> Command {
Command::new(crate::deckcraft_cli()) Command::new(env!("CARGO_BIN_EXE_deckcraft-cli"))
} }
fn run(args: &[&str]) -> Output { fn run(args: &[&str]) -> Output {
+4 -1
View File
@@ -251,7 +251,10 @@ pub fn sidecar_part(name: &str) -> bool {
let file = segs.last().copied().unwrap_or(""); let file = segs.last().copied().unwrap_or("");
let stem = file.strip_prefix("chart").unwrap_or(""); let stem = file.strip_prefix("chart").unwrap_or("");
let num = stem.strip_suffix(".xml.rels").or_else(|| stem.strip_suffix(".xml")).unwrap_or(""); let num = stem.strip_suffix(".xml.rels").or_else(|| stem.strip_suffix(".xml")).unwrap_or("");
!(stem != file && !num.is_empty() && num.chars().all(|c| c.is_ascii_digit())) if stem != file && !num.is_empty() && num.chars().all(|c| c.is_ascii_digit()) {
return false;
}
true
} }
pub fn dir_of(part: &str) -> String { pub fn dir_of(part: &str) -> String {
+9
View File
@@ -1,2 +1,11 @@
sonar.projectKey=storytold_deckcraft sonar.projectKey=storytold_deckcraft
sonar.organization=storytold sonar.organization=storytold
# githubactions:S7637 asks for every action to be pinned to a commit SHA. A workflow covered by
# .github/workflows/actions.lock runs the commit the lockfile records, so its tag refs are pinned
# and the lockfile covers the actions those actions use too. Sonar does not read lockfiles yet, so
# the rule flags the tag refs anyway:
# https://community.sonarsource.com/t/githubactions-s7637-false-positives-for-actions-pinned-through-native-actions-lock/190814
sonar.issue.ignore.multicriteria=gha-lockfile
sonar.issue.ignore.multicriteria.gha-lockfile.ruleKey=githubactions:S7637
sonar.issue.ignore.multicriteria.gha-lockfile.resourceKey=.github/workflows/**